Melhore os resultados de pesquisa do seu aplicativo com o ajuste automático
Avalie esse Tutorial
Historicamente, a única maneira de melhorar a relevância da consulta de pesquisa do seu aplicativo é por meio de intervenção manual. Por exemplo, você pode introduzir oaumento de pontuação para multiplicar uma pontuação básica de relevância na presença de campos específicos. Isso garante que as pesquisas em que uma chave presente em alguns campos tenham um peso maior do que em outras. Isso, no entanto, é fixo por natureza. Os resultados são dinâmicos, mas a lógica em si não muda.
O projeto a seguir mostrará como aproveitar sinônimos para criar um loop de feedback auto-ajustável, a fim de fornecer resultados de pesquisa cada vez mais relevantes aos usuários, tudo sem modelos complexos de aprendizado de máquina!
Temos um aplicativo de busca de alimentos em que um usuário pesquisa por “Romanian Food.”. Supondo que estejamos registrando os dados de sequência de cliques de cada usuário (sua interação passo a passo com nosso aplicativo), podemos dar uma olhada nesse “sequence” e compará-lo com outros resultados que produziram um forte CTA (call to action): um checkout bem-sucedido.
Outro usuário pesquisou por "German Cuisine " e ele tinha uma sequência de cliques muito semelhante. Bem, podemos criar um script que analise os fluxos de cliques desses usuários (e de outros usuários), identificando similaridades, podemos dizer ao script para anexá-lo a um documento de sinônimos que contenha "German, " "Romanian, " e outros cozinhas mais comuns, como “Hungarian.”
Aqui está um fluxo de trabalho do que pretendemos realizar:
Em nossa camada de aplicativo, à medida que os eventos são disparados, nós os registramos em uma coleção de clickstreams, como:
1 [{ 2 "session_id": "1", 3 "event_id": "search_query", 4 "metadata": { 5 "search_value": "romanian food" 6 }, 7 "timestamp": "1" 8 }, 9 { 10 "session_id": "1", 11 "event_id": "add_to_cart", 12 "product_category":"eastern european cuisine", 13 "timestamp": "2" 14 }, 15 { 16 "session_id": "1", 17 "event_id": "checkout", 18 "timestamp": "3" 19 }, 20 { 21 "session_id": "1", 22 "event_id": "payment_success", 23 "timestamp": "4" 24 }, 25 { 26 "session_id": "2", 27 "event_id": "search_query", 28 "metadata": { 29 "search_value": "hungarian food" 30 }, 31 "timestamp": "1" 32 }, 33 { 34 "session_id": "2", 35 "event_id": "add_to_cart", 36 "product_category":"eastern european cuisine", 37 "timestamp": "2" 38 } 39 ]
Nesta lista simplificada de eventos, podemos concluir que {"session_id":"1"} pesquisou "romanian food, " o que levou a uma taxa de conversão mais alta, deployment_success, em comparação com {"session_id":"2"}, que pesquisou "hungarian food " e parou após o evento add_to_cart. Você mesmo pode importar esses dados usando sample_data.json.
Vamos preparar os dados para nosso script search_tuner.
A propósito, não há problema que apenas alguns documentos tenham um campo de metadados. Nosso operador $group pode identificar de forma inteligente os que fazem e os que não fazem isso.
1 [ 2 # first we sort by timestamp to get everything in the correct sequence of events, 3 # as that is what we'll be using to draw logical correlations 4 { 5 '$sort': { 6 'timestamp': 1 7 } 8 }, 9 # next, we'll group by a unique session_id, include all the corresponding events, and begin 10 # the filter for determining if a search_query exists 11 { 12 '$group': { 13 '_id': '$session_id', 14 'events': { 15 '$push': '$$ROOT' 16 }, 17 'isSearchQueryPresent': { 18 '$sum': { 19 '$cond': [ 20 { 21 '$eq': [ 22 '$event_id', 'search_query' 23 ] 24 }, 1, 0 25 ] 26 } 27 } 28 } 29 }, 30 # we hide session_ids where there is no search query 31 # then create a new field, an array called searchQuery, which we'll use to parse 32 { 33 '$match': { 34 'isSearchQueryPresent': { 35 '$gte': 1 36 } 37 } 38 }, 39 { 40 '$unset': 'isSearchQueryPresent' 41 }, 42 { 43 '$set': { 44 'searchQuery': '$events.metadata.search_value' 45 } 46 } 47 ]
Vamos criar a visualização criando a query, depois acessando o Compass e adicionando-a como uma nova collection chamada group_by_session_id_and_search_query:
Veja como será:
1 [ 2 { 3 "session_id": "1", 4 "events": [ 5 { 6 "event_id": "search_query", 7 "search_value": "romanian food" 8 }, 9 { 10 "event_id": "add_to_cart", 11 "context": { 12 "cuisine": "eastern european cuisine" 13 } 14 }, 15 { 16 "event_id": "checkout" 17 }, 18 { 19 "event_id": "payment_success" 20 } 21 ], 22 "searchQuery": "romanian food" 23 }, { 24 "session_id": "2", 25 "events": [ 26 { 27 "event_id": "search_query", 28 "search_value": "hungarian food" 29 }, 30 { 31 "event_id": "add_to_cart", 32 "context": { 33 "cuisine": "eastern european cuisine" 34 } 35 }, 36 { 37 "event_id": "checkout" 38 } 39 ], 40 "searchQuery": "hungarian food" 41 }, 42 { 43 "session_id": "3", 44 "events": [ 45 { 46 "event_id": "search_query", 47 "search_value": "italian food" 48 }, 49 { 50 "event_id": "add_to_cart", 51 "context": { 52 "cuisine": "western european cuisine" 53 } 54 } 55 ], 56 "searchQuery": "sad food" 57 } 58 ]
1 // Provide a success indicator to determine which session we want to 2 // compare any incomplete sessions with 3 const successIndicator = "payment_success" 4 5 // what percentage similarity between two sets of click/event streams 6 // we'd accept to be determined as similar enough to produce a synonym 7 // relationship 8 const acceptedConfidence = .9 9 10 // boost the confidence score when the following values are present 11 // in the eventstream 12 const eventBoosts = { 13 successIndicator: .1 14 } 15 16 /** 17 * Enrich sessions with a flattened event list to make comparison easier. 18 * Determine if the session is to be considered successful based on the success indicator. 19 * @param {*} eventList List of events in a session. 20 * @returns {any} Calculated values used to determine if an incomplete session is considered to 21 * be related to a successful session. 22 */ 23 const enrichEvents = (eventList) => { 24 return { 25 eventSequence: eventList.map(event => { return event.event_id }).join(';'), 26 isSuccessful: eventList.some(event => { return event.event_id === successIndicator }) 27 } 28 } 29 30 /** 31 * De-duplicate common tokens in two strings 32 * @param {*} str1 33 * @param {*} str2 34 * @returns Returns an array with the provided strings with the common tokens removed 35 */ 36 const dedupTokens = (str1, str2) => { 37 const splitToken = ' ' 38 const tokens1 = str1.split(splitToken) 39 const tokens2 = str2.split(splitToken) 40 const dupedTokens = tokens1.filter(token => { return tokens2.includes(token)}); 41 const dedupedStr1 = tokens1.filter(token => { return !dupedTokens.includes(token)}); 42 const dedupedStr2 = tokens2.filter(token => { return !dupedTokens.includes(token)}); 43 44 return [ dedupedStr1.join(splitToken), dedupedStr2.join(splitToken) ] 45 } 46 47 const findMatchingIndex = (synonyms, results) => { 48 let matchIndex = -1 49 for(let i = 0; i < results.length; i++) { 50 for(const synonym of synonyms) { 51 if(results[i].synonyms.includes(synonym)){ 52 matchIndex = i; 53 break; 54 } 55 } 56 } 57 return matchIndex; 58 } 59 /** 60 * Inspect the context of two matching sessions. 61 * @param {*} successfulSession 62 * @param {*} incompleteSession 63 */ 64 const processMatch = (successfulSession, incompleteSession, results) => { 65 console.log(`=====\nINSPECTING POTENTIAL MATCH: ${ successfulSession.searchQuery} = ${incompleteSession.searchQuery}`); 66 let contextMatch = true; 67 68 // At this point we can assume that the sequence of events is the same, so we can 69 // use the same index when comparing events 70 for(let i = 0; i < incompleteSession.events.length; i++) { 71 // if we have a context, let's compare the kv pairs in the context of 72 // the incomplete session with the successful session 73 if(incompleteSession.events[i].context){ 74 const eventWithContext = incompleteSession.events[i] 75 const contextKeys = Object.keys(eventWithContext.context) 76 77 try { 78 for(const key of contextKeys) { 79 if(successfulSession.events[i].context[key] !== eventWithContext.context[key]){ 80 // context is not the same, not a match, let's get out of here 81 contextMatch = false 82 break; 83 } 84 } 85 } catch (error) { 86 contextMatch = false; 87 console.log(`Something happened, probably successful session didn't have a context for an event.`); 88 } 89 } 90 } 91 92 // Update results 93 if(contextMatch){ 94 console.log(`VALIDATED`); 95 const synonyms = dedupTokens(successfulSession.searchQuery, incompleteSession.searchQuery, true) 96 const existingMatchingResultIndex = findMatchingIndex(synonyms, results) 97 if(existingMatchingResultIndex >= 0){ 98 const synonymSet = new Set([...synonyms, ...results[existingMatchingResultIndex].synonyms]) 99 results[existingMatchingResultIndex].synonyms = Array.from(synonymSet) 100 } 101 else{ 102 const result = { 103 "mappingType": "equivalent", 104 "synonyms": synonyms 105 } 106 results.push(result) 107 } 108 109 } 110 else{ 111 console.log(`NOT A MATCH`); 112 } 113 114 return results; 115 } 116 117 /** 118 * Compare the event sequence of incomplete and successful sessions 119 * @param {*} successfulSessions 120 * @param {*} incompleteSessions 121 * @returns 122 */ 123 const compareLists = (successfulSessions, incompleteSessions) => { 124 let results = [] 125 for(const successfulSession of successfulSessions) { 126 for(const incompleteSession of incompleteSessions) { 127 // if the event sequence is the same, let's inspect these sessions 128 // to validate that they are a match 129 if(successfulSession.enrichments.eventSequence.includes(incompleteSession.enrichments.eventSequence)){ 130 processMatch(successfulSession, incompleteSession, results) 131 } 132 } 133 } 134 return results 135 } 136 137 const processSessions = (sessions) => { 138 // console.log(`Processing the following list:`, JSON.stringify(sessions, null, 2)); 139 // enrich sessions for processing 140 const enrichedSessions = sessions.map(session => { 141 return { ...session, enrichments: enrichEvents(session.events)} 142 }) 143 // separate successful and incomplete sessions 144 const successfulEvents = enrichedSessions.filter(session => { return session.enrichments.isSuccessful}) 145 const incompleteEvents = enrichedSessions.filter(session => { return !session.enrichments.isSuccessful}) 146 147 return compareLists(successfulEvents, incompleteEvents); 148 } 149 150 /** 151 * Main Entry Point 152 */ 153 const main = () => { 154 const results = processSessions(eventsBySession); 155 console.log(`Results:`, results); 156 } 157 158 main(); 159 160 module.exports = processSessions;
1 [ 2 { 3 '$search': { 4 'index': 'synonym-search', 5 'text': { 6 'query': 'hungarian', 7 'path': 'cuisine-type' 8 }, 9 'synonyms': 'similarCuisines' 10 } 11 } 12 ]
Aqui está, pessoal. Pegamos dados brutos gravados em nosso servidor de aplicativos e os usamos criando um feedback que incentiva o comportamento positivo do usuário.
Ao medir esse ciclo de feedback em relação aos seus KPIs, você pode criar um teste A/B simples em relação a determinados sinônimos e padrões de usuário para otimizar seu aplicativo!