Upload
hoangnhan
View
221
Download
0
Embed Size (px)
Citation preview
BACKGROUND
5
SuplaAudio podcast application for Finland’s largest commercial radio broadcasting company producing both original content and on-demand episodes of radio programming.
Awarded Best Digital Implementation two years in a row.
REAKTOR
INGREDIENTS OF SEARCH
7
Ingredients for “Good” Search UX
REAKTOR RECRUITING SINCE 2000
ContentUI Index
yours truly
I type some words andthe app gives me relevant content despite minor spelling differences, sorted in a way that makes sense.
GOOD SEARCH UX
10
Search UI Do’s and Don’ts
• Don’t block the UI thread with searches.
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
10
Search UI Do’s and Don’ts
• Don’t block the UI thread with searches.
• Don’t block the database with updates.
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
10
Search UI Do’s and Don’ts
• Don’t block the UI thread with searches.
• Don’t block the database with updates.
• Don’t block new searches with old ones.
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
10
Search UI Do’s and Don’ts
• Don’t block the UI thread with searches.
• Don’t block the database with updates.
• Don’t block new searches with old ones.
• Don’t delay showing results any longer than you must.
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
10
Search UI Do’s and Don’ts
• Don’t block the UI thread with searches.
• Don’t block the database with updates.
• Don’t block new searches with old ones.
• Don’t delay showing results any longer than you must.
• Indicate activity while a search is ongoing.
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
10
Search UI Do’s and Don’ts
• Don’t block the UI thread with searches.
• Don’t block the database with updates.
• Don’t block new searches with old ones.
• Don’t delay showing results any longer than you must.
• Indicate activity while a search is ongoing.
• Differentiate “no matches” from “didn’t do anything”.
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
• Starting position of first match
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
• Starting position of first match
• Full word match vs prefix match
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
• Starting position of first match
• Full word match vs prefix match
• Match in title vs match in body
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
• Starting position of first match
• Full word match vs prefix match
• Match in title vs match in body
• Date of matching document
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
• Starting position of first match
• Full word match vs prefix match
• Match in title vs match in body
• Date of matching document
• Popularity amongst other users
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
11
Ranking Makes a Good Search Great• Number of
occurrences• Length of
match vs length of text
• Starting position of first match
• Full word match vs prefix match
• Match in title vs match in body
• Date of matching document
• Popularity amongst other users
• Behavioural conditioning
REAKTOR RECRUITING SINCE 2000
GOOD SEARCH UX
12
Don’t make me scrollThe goal is to present relevant matches in a sensible order, minimising the need to scroll down the search results.
You know more about your data than Google does about theirs. Use that knowledge to your advantage.
REAKTOR
SQLITE FTS
17
SQLite FTS4 tables: Indexing documents// create an FTS table for the index CREATE VIRTUAL TABLE conferences USING fts4(name, tagline, tokenize=icu fi_FI);
// add a document to the index INSERT INTO conferences (docid, name, tagline) VALUES(42, 'CodeMobile', 'Pure awesomeness');
// optimise the index when the app is idle INSERT INTO conferences(conferences) VALUES('optimize');
REAKTOR OCTOBER 2015 — CONFIDENTIAL
SQLITE FTS
18
SQLite FTS4 tables: Searching Documents -- search across all columns, order by "matchinfo"01 SELECT * FROM conferences WHERE conferences 02 MATCH 'ios* OR android* OR mobile*' 03 ORDER BY matchinfo(conferences) DESC;
REAKTOR
SQLITE FTS
19
SQLite FTS4 tables: Searching documents
02 JOIN (03 SELECT docid,04 rank(matchinfo(conferences,'pcxnal'), 10, 1) AS rank05 -- 'rank' is a custom SQL function06 FROM conferences WHERE conferences MATCH 'ios* OR android*'07 ORDER BY rank DESC LIMIT 1000 OFFSET 009 ) AS ranktable USING(docid)1011 ORDER BY ranktable.rank DESC
01 SELECT name, docid FROM conferences 02 03 04 05 06 07 09 10 WHERE conferences MATCH 'ios* OR android*'
REAKTOR
CLUCENE + BRFULLTEXTSEARCH
21
CLucene + BRFullTextSearch: Indexing documents#import <BRFullTextSearch/BRFullTextSearch.h> #import <BRFullTextSearch/CLuceneSearchService.h> …
let lucene = CLuceneSearchService(indexPath: "MyIndex.index") …
let fields = [ "t": "CodeMobile 2017, April 18-20", "v": "Fantastic conference in Chester, UK" ] let doc = BRSimpleIndexable(identifier: "doc1", data: fields) lucene.addObjectToIndex(doc, queue: nil, finished: nil)
REAKTOR
CLUCENE + BRFULLTEXTSEARCH
22
CLucene + BRFullTextSearch: Searchinglet query = "t:(surf* OR board*) OR v:(surf* OR board*)"
let boostedQuery = "t:(\"surf*\") OR v:(\"board*\"^10)"
REAKTOR
CLUCENE + BRFULLTEXTSEARCH
22
CLucene + BRFullTextSearch: Searchinglet query = "t:(surf* OR board*) OR v:(surf* OR board*)"
let boostedQuery = "t:(\"surf*\") OR v:(\"board*\"^10)"
let results = lucene.search(query)
results.iterateWithBlock({ (i, result, stop) in print("Match: \(result.identifier): \(result.dictionaryRepresentation())") })
REAKTOR
REAKTOR JOIN US AT REAKTOR.COM/CAREERS
Core Spotlightimport CoreSpotlight
CORE SPOTLIGHT
24
Core Spotlight: Indexing documentslet type = kUTTypeAudio as String // Tells iOS the type of content let attributeSet = CSSearchableItemAttributeSet(itemContentType: type) attributeSet.kind = "..." // A name for the content type attributeSet.title = "..." // This is the indexed title attributeSet.displayName = "..." // This is the displayed title attributeSet.textContent = "..." // This is the indexed description attributeSet.contentDescription = "..." // This is the displayed descriptionattributeSet.thumbnailURL = "..." // The image displayed in search
let item = CSSearchableItem(uniqueIdentifier: "video1", domainIdentifier: "videos", attributeSet: attributeSet) item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 15)
REAKTOR
CORE SPOTLIGHT
25
Core Spotlight: Indexing documentslet index = CSSearchableIndex.default() index.indexSearchableItems( [ item1, item2, item3 ] )
index.deleteAllSearchableItems()
index.deleteSearchableItems(withIdentifiers: [“video1"])
index.deleteSearchableItems(withDomainIdentifiers: ["media.videos.cats"])
REAKTOR
CORE SPOTLIGHT
26
Core Spotlight: reacting to Spotlight searchfunc application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { if let userInfo = userActivity.userInfo, let id = userInfo[CSSearchableItemActivityIdentifier] as? String { // id now contains "video1" which we can lookup from our repository // and launch in the UI handleLaunchFromCoreSpotlight(id) return true } return false }
REAKTOR
COMPOUNDS
31REAKTOR WE MAKE APPS AND LAUNCH SATELLITES
01 def split(word, dictionary):02 splits, min_length = [], 303 if len(word) >= (min_length * 2):04 for i in range(min_length, len(word) - min_length):05 head, tail = word[:i], word[i:]06 if (head in dictionary):07 if (tail in dictionary):08 splits.append(head + " " + tail)09 splits.extend(split(tail, dictionary))10 return splits
COMPOUNDS
32REAKTOR WE MAKE APPS AND LAUNCH SATELLITES
func split(_ word: String, dict: SplittingDictionary, seed: String? = nil) -> [String] { let minimumWordLength = 2 var splits = [String]() guard word.characters.count >= (2*minimumWordLength) else { return splits } (minimumWordLength...(word.characters.count-minimumWordLength)).forEach { offset in let head = word.substring(to: word.index(word.startIndex, offsetBy: offset)) let isPrefix = dict.contains(prefix: head) if dict.contains(word: head) || isPrefix { let tail = word.substring(from: word.index(word.startIndex, offsetBy: offset)) if dict.contains(word: tail) { let inclHead: String? = isPrefix ? nil : head splits.append([seed, inclHead, tail].flatMap { $0 }.joined(separator: " ")) } let tailDict = dict.withoutPrefixes() let tailSeed = [seed, head].flatMap { $0 }.joined(separator: " ") splits.append(contentsOf: split(tail, using: tailDict, seed: tailSeed)) } } return splits}
COMPOUNDS
33REAKTOR WE MAKE APPS AND LAUNCH SATELLITES
windsurfing " w + indsurfingwindsurfing " wi + ndsurfingwindsurfing " win + dsurfing ✗ only HEAD matcheswindsurfing " wind + surfing ✓ HEAD and TAIL match!windsurfing " winds + urfing ✗ only HEAD matcheswindsurfing " windsu + rfingwindsurfing " windsur + fingwindsurfing " windsurf + ing ✗ only HEAD matcheswindsurfing " windsurfi + ngwindsurfing " windsurfin + g
windsurfing ~ windsurfbunnies ~ bunniThe Snowball Project - http://showballstem.org
STEMMING
34
Stemming reduces a word to its base (a stem)
REAKTOR
STEMMING
35
Stemming reduces a word to its base (a stem)#import "libstemmer.h" // http://showballstem.org @implementation SnowballStemmer - (NSString *)stem:(NSString *)word { struct sb_stemmer * stemmer = sb_stemmer_new("en", "UTF_8"); const char * originalWordCString = [word UTF8String]; unsigned long originalLength = strlen(originalWordCString); const sb_symbol * stemmedWord = sb_stemmer_stem(stemmer, (const sb_symbol *)originalWordCString, (int) originalLength); return [NSString stringWithUTF8String:(const char *)stemmedWord]; } @end
REAKTOR
windsurfing ~ windsurfingbunnies ~ bunny
LEMMATISATION
36
lemmatisation turns a word to its basic form(a lemma)
REAKTOR
LEMMATISATION
37
lemmatisation turns a word to its basic form(a lemma)
// pod 'Parsimmon' - wraps NSLinguisticTaggerimport Parsimmon let phrase = "I'm eating chocolate bunnies." let stems = Lemmatizer().lemmatizeWordsInText(phrase) print(stems) // => ["I", "eat", "chocolate", "bunny"]
REAKTOR
I type some words andthe app gives me relevant contentdespite minor spelling differences,sorted in a way that makes sense.