background - codemobile · background 5 supla audio podcast application for finland’s largest...

51

Upload: hoangnhan

Post on 20-Jun-2019

216 views

Category:

Documents


0 download

TRANSCRIPT

BACKGROUND

4

18 apps and counting

REAKTOR OCTOBER 2015 — CONFIDENTIAL

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

BACKGROUND

SEARCH

6REAKTOR WE MAKE APPS AND LAUNCH SATELLITES

INGREDIENTS OF SEARCH

7

Ingredients for “Good” Search UX

REAKTOR RECRUITING SINCE 2000

ContentUI Index

GOOD SEARCH UX

8

Ingredients for “Good” Search UX

REAKTOR RECRUITING SINCE 2000

UI

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

REAKTOR RECRUITING SINCE 2000

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

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

INGREDIENTS OF SEARCH

13REAKTOR WE MAKE APPS AND LAUNCH SATELLITES

ContentUI Index

INDEX

14

Ingredients for “Good” local Search UX

REAKTOR RECRUITING SINCE 2000

Index

Why not use[insert database product here]?

REAKTOR JOIN US AT REAKTOR.COM/CAREERS

SQLite FTS tables

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

REAKTOR JOIN US AT REAKTOR.COM/CAREERS

CLucenepod ‘BRFullTextSearch’

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

INGREDIENTS OF SEARCH

27

Ingredients for “Good” Search UX

REAKTOR

ContentUI Index

28REAKTOR WE MAKE APPS AND LAUNCH SATELLITES

Content

COMPOUNDS

30REAKTOR WE MAKE APPS AND LAUNCH SATELLITES

surf

board

surfboard

boa

boar

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.