ݺߣ

ݺߣShare a Scribd company logo
Повнотекстовий пошук: кішочки
Проблема
Давним-давно, в далекій-далекій галактиці…
...був проект на Magento, у якого були проблеми з пошуком...
Проблема
Серйозні
проблеми...
Наївне розв’язання
Я реалізував рішення, яке працювало:
SELECT * FROM products WHERE title LIKE '%ipod%' OR title LIKE '%nano%' OR
title LIKE '%ipod nano%' OR description LIKE '%ipod%' OR description LIKE
'%nano%' OR description LIKE '%ipod nano%';
Наївне розв’язання
Яке насправді більш схоже було на це:
SELECT product_id FROM (
SELECT
products.id AS product_id,
2 AS score
FROM products
WHERE title LIKE '%ipod nano%' OR description LIKE '%ipod nano%'
UNION
SELECT
products.id,
1 AS score
FROM products
WHERE title LIKE '%ipod%' OR title LIKE '%nano%' OR description LIKE '%ipod%' OR description LIKE '%nano%'
ORDER BY
score DESC
) AS T1;
Наївне розв’язання
Яке насправді більш схоже було на це:
SELECT product_id FROM (
SELECT
products.id AS product_id,
2 AS score
FROM products
WHERE title LIKE '%ipod nano%' OR description LIKE '%ipod nano%'
UNION
SELECT
products.id,
1 AS score
FROM products
WHERE title LIKE '%ipod%' OR title LIKE '%nano%' OR description LIKE '%ipod%' OR description LIKE '%nano%'
ORDER BY
score DESC
) AS T1;
І воно реально працювало
на продакшні!
Наївне розв’язання
Спробуйте вгадати, які були проблеми у такого підходу 😉
1. Перформанс
Довгі пошукові запити, багато продуктів, довгі описи продуктів - все
приводило до деградації видайності.
Проблеми наївного підходу
2. Все ще “погані” результати пошуку (принаймні, не перша сторінка)
Проблеми наївного підходу
Проблеми наївного підходу
3. Складніші кейси не підтримувались
Помилки при наборі тексту (typos), варіації слів (cables ↔ cable), скорочення
(S.T.A.L.K.E.R. ↔ stalker), вже не говорячи про синоніми та ін.
Проблеми наївного підходу
Поки воно працює - не чіпай його!
Як працюють всі ці гугли?
І ось, нещодавно, я відкрив для себе цілий світ науки про Information
Retrieval
Сьогодні в програмі
Information Retrieval - досить обширний і глибокий розділ науки, тож я розкажу
тільки про такі (цікаві?) теми:
● Як зберігати дані
● Як сортувати результати
● Нечіткий пошук
Сьогодні в програмі
Гуглити за такими запитами:
● Inverted index
● TF-IDF
● Vector-space model
● Levenshtein & Levenshtein-Damerau distance
Декілька понять
● Документ
● Терм
● Індекс
Документ
Документ - це текст, який ми скормлюємо пошуковому двіжку разом з його
айдішником (тексту), щоб пізніше зрозуміти що конкретно було знайдено.
A complete numerical series is followed by
an incomplete numerical series. You need
to solve that incomplete numerical series in
the same pattern in which the complete
numerical series is given.
оце все - документ
Терм
Терм - це одне слово, фраза або скорочення з документу.
A complete numerical series is followed by
an incomplete numerical series. You need
to solve that incomplete numerical series in
the same pattern in which the complete
numerical series is given.
Кожне з них - терм
Індекс
Індекс - це структура даних, в якій ми зберігаємо документи, для пошуку.
{
"numerical": 4,
"series": 4,
"is": 2,
"incomplete": 2,
"complete": 2,
"in": 2,
"the": 2,
"need": 1,
"a": 1,
"you": 1,
"followed": 1,
"to": 1,
...
}
Нахіба нам індекс?
Табличка в базі даних:
1 -> long long long text
2 -> shorter but still long text
3 -> extremely, unbelievably long text
Inverted index:
term -> 1, 3, 155, 20195
other_term -> 2, 3, 14, 29, 31, 20114
rare_term -> 19852, 30421
Нахіба нам індекс?
Табличка в базі даних:
1 -> long long long text
2 -> shorter but still long text
3 -> extremely, unbelievably long text
Inverted index:
term -> 1, 3, 155, 20195
other_term -> 2, 3, 14, 29, 31, 20114
rare_term -> 19852, 30421
Запит буде порівняно з кожним
документом, по черзі.
Для кожного терму з запиту
можна досить швидко
витягнути всі документи, які
мають в собі цей терм.
Нахіба нам індекс?
Табличка в базі даних:
1 -> long long long text
2 -> shorter but still long text
3 -> extremely, unbelievably long text
Inverted index:
term -> 1, 3, 155, 20195
other_term -> 2, 3, 14, 29, 31, 20114
rare_term -> 19852, 30421
Нахіба нам індекс?
Пошук в Lucene Пошук в БД
Нахіба нам індекс?
Нахіба нам індекс?
Додавання і видалення даних в/з індексу повільне, але пошук - швидкий
Inverted index: деякі реалізації
Map<String, List<Long>> - кожному терму відповідає список айдішників
документів, які мають цей терм.
{
"term1": ["doc1", "doc2"],
"term2": ["doc2", "doc3"]
}
Map<String, Map<String, Long>> - кожному терму відповідає список
айдішників документів і кількість цього терму в цьому документі. Розширена
версія, (трошки) прискорює обчислення (див. далі)
{
"term1": { "doc1": 1, "doc2": 3 },
"term2": { "doc2": 4, "doc3": 1 }
}
Inverted index: деякі реалізації
Matrix2D<Long> - двовимірний масив, кожному айдішнику документа (одна
вісь матриці) відповідає кількість терма (друга вісь матриці) в цьому
документі. Швидкі обчислення, але жере купу лишньої пам’яті.
Inverted index: деякі реалізації
doc1 doc2 doc3
term1 1 3 0
term2 0 4 1
Inverted index: деякі реалізації
Map<String, Map<String, List<Long>>> - терм - документ і позиції
цього терма в цьому документі. Дозволяє шукати по фразах.
{
"term1": { "doc1": [0], "doc2": [0,2,6] },
"term2": { "doc2": [1,3,4,5], "doc3": [0] }
}
Створюємо inverted index
1. Розбиваємо текст на слова (отримаємо список слів; це називається
“аналіз документу”)
2. Заповнюємо хешмапу індексу відповідно (як ключ беремо слово, як
значення робимо або список ID документів, або кількість цього слова в
документі, або шо)
Для оптимізації, можна зліпити ці два кроки в один цикл/map-reduce.
Пошук по індексу
Дано:
● List<String> - наш пошуковий запит; слова які ТРЕБА або БАЖАНО
щоб були у знайдених документах
● Index - індекс 😐
const result = new Set();
for (let queryTerm of query) {
const docs = index.get(queryTerm);
for (let docId of docs) {
result.add(docId);
}
}
Пошук в індексі: шукаємо БУДЬ-ЯКИЙ терм
Пошук в індексі: шукаємо БУДЬ-ЯКИЙ терм
const result = [];
for (let i = 0; i < query.length; ++i) {
result = _.union(index.get(query[i]));
}
const result = new Set(index.get(query[0]));
for (let i = 1; i < query.length; ++i) {
const queryTerm = query[i];
const docs = index.get(queryTerm);
for (let docId of result) {
if (!docs.has(docId)) {
result.delete(docId);
}
}
}
Пошук в індексі: шукаємо ВСІ терми
Пошук в індексі: шукаємо ВСІ терми
const result = index.get(query[0]);
for (let i = 1; i < query.length; ++i) {
result = _.intersection(index.get(query[i]));
}
Пошук: крок 2
Знайти документи не складно. Складно посортувати їх шоб було гарненько.
TF-IDF
TF - term frequency
IDF - inverse document frequency
Число, яке показує наскільки терм важливий для конкретного документу
TF-IDF
TF-IDF
Нехай у нас є такі два тексти:
A complete numerical series is followed by
an incomplete numerical series. You need
to solve that incomplete numerical series in
the same pattern in which the complete
numerical series is given.
Farscape is my favourite series. It’s unique
and, as far as i know, there is no similar
series out there, so give it a try if you want
to see something unique. Some say Firefly,
Andromeda, Lexx and so on are similar.
But those series are very different and
share nothing in common with farscape in
my opinion.
Наскільки релевантне слово “series” для кожного з них?
TF-IDF
TF-IDF
Вот еті рєбята:
A complete numerical series is followed by
an incomplete numerical series. You need
to solve that incomplete numerical series in
the same pattern in which the complete
numerical series is given.
Farscape is my favourite series. It’s unique
and, as far as i know, there is no similar
series out there, so give it a try if you want
to see something unique. Some say Firefly,
Andromeda, Lexx and so on are similar.
But those series are very different and
share nothing in common with farscape in
my opinion.
Примітка: TF-IDF простіше рахувати використовуючи розширені варіанти
індексу, як от цей:
// term -> { documentId -> number of occurrences }
const index: Map<String, Map<Number, Number>>;
// documentId -> [ term ]
const documents: Map<Number, List<String>>;
TF-IDF
TF
Глянем на аналіз цих двох текстів:
{
"series": 3,
"and": 3,
"there": 2,
"unique": 2,
"farscape": 2,
"as": 2,
"my": 2,
"similar": 2,
"so": 2,
"is": 2,
"are": 2,
"in": 2,
"favourite": 1
}
{
"numerical": 4,
"series": 4,
"is": 2,
"incomplete": 2,
"complete": 2,
"in": 2,
"the": 2,
"need": 1,
"a": 1,
"you": 1,
}
TF
tf = (term, docId) => index.get(term).get(docId) / documents.get(docId).length;
Співвідношення кількості цього терма в документі до кількості всіх слів в
документі.
TF-IDF
Індекс для кожного з цих текстів:
A complete numerical series is followed by
an incomplete numerical series. You need
to solve that incomplete numerical series in
the same pattern in which the complete
numerical series is given.
{
"numerical": 4,
"series": 4,
"is": 2,
"incomplete": 2,
"complete": 2,
"in": 2,
"the": 2,
"need": 1,
"a": 1,
"you": 1,
}
TF-IDF
Farscape is my favourite series. It’s unique
and, as far as i know, there is no similar
series out there, so give it a try if you want
to see something unique. Some say Firefly,
Andromeda, Lexx and so on are similar.
But those series are very different and
share nothing in common with farscape in
my opinion.
{
"series": 3,
"and": 3,
"there": 2,
"unique": 2,
"farscape": 2,
"as": 2,
"my": 2,
"similar": 2,
"so": 2,
"is": 2,
"are": 2,
"in": 2,
"favourite": 1
}
Індекс для кожного з цих текстів:
IDF
А тепер зиркнемо на весь індекс:
{
"a": [1, 2],
"series": [1, 2],
"is": [1, 2],
"you": [1, 2],
"to": [1, 2],
"in": [1, 2],
"an": [1],
"incomplete": [1],
"opinion": [2],
"need": [1],
"numerical": [1],
}
IDF
idf = (term) => Math.log(index.size / index.get(term).size)
Логарифм від співвідношення кількості слів в індексі до кількості документів, в
яких є даний терм.
// term -> { documentId -> number of occurrences }
let index: Map<String, Map<Number, Number>>;
// documentId -> [ term ]
let documents: Map<Number, List<String>>;
tf = (term, docId) => index.get(term).get(docId) / documents.get(docId).length;
idf = (term) => Math.log(1 + (index.size / index.get(term).size));
tf_idf = (term, docId) => tf(term, docId) * idf(term);
TF-IDF
tf_ifd("series", doc1) = 0.43886417827898777
tf_idf("series", doc2) = 0.17592400249976667
Тобто, слово “series” значно
менш релевантне для другого
документу
TF-IDF
tf_ifd("series", doc1) = 0.43886417827898777
tf_idf("series", doc2) = 0.17592400249976667
Тобто, слово “series” значно
менш релевантне для другого
документу
ШТА?
TF-IDF
А проблеми роблять ось такі штуки:
{
"and": 3, "there": 2, "as": 2, "my": 2,
"so": 2, "is": 2, "are": 2, "in": 2,
"it’s": 1, "it": 1, "a": 1, "if": 1,
"to": 1, "on": 1, "i": 1, "but": 1,
"those": 1, "with": 1
}
TF-IDF
tf_ifd("series", doc1) = 0.6040849772288726
tf_idf("series", doc2) = 0.2517020738453635
TF-IDF: фільтруємо слова-паразити
TF-IDF це канєшно круто, але воно працює тільки для одного слова і одного
документа. А якшо ми робим гугль, і у нас ТИСЯЧІ, МІЛЬЙОНИ документів?
Чутка математики
Vector-space model
Вумні дядьки взяли і використали вектори (по суті - просто масив чисел, з
парою хитрих операцій).
Vector-space model
Але ці дядьки (як на мене) лажанулись, намагаючись пояснити цю штуку
Vector-space model
Але ці дядьки (як на мене) лажанулись, намагаючись пояснити цю штуку
Vector-space model
Але ці дядьки (як на мене) лажанулись, намагаючись пояснити цю штуку
Vector-space model
Vector-space model
Якщо ми представимо кожен документ і запит як вектор, ми зможемо
визначити наскільки вони схожі, взявши косинус кута між ними.
Vector-space model
Безполєзно малювати ще одну картінку, так шо хавай, піпл, табличку:
term  doc ID doc1 doc2 query
series 1 1 1
you 1 1 0
numerical 1 0 0
andromeda 0 1 1
occurrences
Vector-space model
query = "series andromeda";
similarity(doc1, query) = cos((1, 1, 1, 0), (1, 0, 0, 1)) = 0.4
similarity(doc2, query) = cos((1, 1, 0, 1), (1, 0, 0, 1)) = 0.8
term  doc ID doc1 doc2 query
series 1 1 1
you 1 1 0
numerical 1 0 0
andromeda 0 1 1
Vector-space model
query = "numerical series";
similarity(doc1, query) = cos((1, 1, 1, 0), (1, 0, 1, 0)) = 0.8
similarity(doc2, query) = cos((1, 1, 0, 1), (1, 0, 1, 0)) = 0.4
term  doc ID doc1 doc2 query
series 1 1 1
you 1 1 0
numerical 1 0 1
andromeda 0 1 0
Vector-space model
query = "series numerical";
similarity(doc1, query) = cos((1, 1, 1, 0), (1, 0, 1, 0)) = 0.8
similarity(doc2, query) = cos((1, 1, 0, 1), (1, 0, 1, 0)) = 0.4
Проблема в тому, що перестановка слів в запиті не дає нікакого еффекту.
Після всього вищесказаного, у вас має
виникнути одне питання
І чьо?
Ліпим все до купи
class Index {
private index: Map<String, Map<String, Number>>;
// documentId -> [ term ]
private documents: Map<String, Array<String>>;
// documentId -> text
private originals: Map<String, String>;
...
}
similarity(doc1: Array<String>, doc2: Array<String>): Number {
let score = 0;
for (const term of this.index.keys()) {
score += this.tf(term, doc1) * this.tf(term, doc2);
}
return score;
}
Ліпим все до купи
public search(query: String) {
const queryTerms = this.parse(query);
const scores = new Map(); const counts = new Map();
for (const term of queryTerms) {
const matches = this.index.get(term);
for (const docId of matches.keys()) {
scores.set(docId, scores.get(docId) + this.similarity(queryTerms, this.documents.get(docId)));
counts.set(docId, counts.get(docId) + 1);
}
}
for (const docId of scores.keys()) {
results.set(docId, scores.get(docId) * counts.get(docId));
}
}
Ліпим все до купи
Demo time!
Demo Drinking time!
Бонус: did you mean: *that*?
Нечіткий пошук
Прикольна фіча, яка робить гугл “розумним” називається “нечіткий пошук”
(fuzzy search) - коли алгоритм знаходить слова, подібні до слова з запиту
(враховуючи всякі очєпяткі, синоніми і т.д.).
Нечіткий пошук
Простий і ефективний алгоритм який рахує наскільки два слова подібні між
собою - відстань Левенштейна (з розширенням, відстань Левенштейна-
Дамерау)
Відстань Левенштейна
Рахує скільки “операцій” треба зробити зі словом А щоб отримати слово Б.
Відстань Левенштейна
Ось ці операції:
● Додати один символ
● Видалити один символ
● Замінити один символ на інший
Відстань Левенштейна
perference -> preference (2 заміни)
perfmance -> performance (2 додання)
battlestart -> battlestar (1 видалення)
levenshtein(s1, s2) {
let v0 = []; let v1 = [];
for (let i = 0; i <= s1.length; ++i) { v0[i] = i; }
for (let i = 0; i < s2.length; ++i) {
v1[0] = i + 1;
for (let j = 0; j < s1.length; ++j) {
v1[j + 1] = Math.min(v0[j + 1] + 1, v0[j] + 1, v0[j] + (s1[i] != s2[j]));
}
let tmp = v0; v0 = v1; v1 = tmp;
}
return v0[s1.length];
}
Відстань Левенштейна
Відстань Левенштейна-Дамерау
Те ж саме, що і відстань Левенштейна, тільки рахує ще одну операцію:
● Додати один символ
● Видалити один символ
● Замінити один символ на інший
● Поміняти два сусідні символи місцями
Відстань Левенштейна-Дамерау
perference -> preference (1 перестановка)
perfmance -> performance (2 додання)
battlestart -> battlestar (1 видалення)
levenshtein_damerau(s1, s2) {
let v0 = []; let v1 = [];
for (let i = 0; i <= s1.length; ++i) { v0[i] = i; }
for (let i = 0; i < s2.length; ++i) {
v1[0] = i + 1;
for (let j = 0; j < s1.length; ++j) {
v1[j + 1] = Math.min(v0[j + 1] + 1, v0[j] + 1, v0[j] + (s1[i] != s2[j] ? 1 : 0));
if (i > 0 && j > 0 && s1[i] == s2[j - 1] && s1[i - 1] == s2[j]) {
v1[j + 1] = Math.min(v1[j + 1], v0[j - 1]);
}
}
let tmp = v0; v0 = v1; v1 = tmp;
}
return v0[s1.length];
}
Відстань Левенштейна-Дамерау
Шо не ясно?

More Related Content

Повнотекстовий пошук - кішочки

  • 2. Проблема Давним-давно, в далекій-далекій галактиці… ...був проект на Magento, у якого були проблеми з пошуком...
  • 4. Наївне розв’язання Я реалізував рішення, яке працювало: SELECT * FROM products WHERE title LIKE '%ipod%' OR title LIKE '%nano%' OR title LIKE '%ipod nano%' OR description LIKE '%ipod%' OR description LIKE '%nano%' OR description LIKE '%ipod nano%';
  • 5. Наївне розв’язання Яке насправді більш схоже було на це: SELECT product_id FROM ( SELECT products.id AS product_id, 2 AS score FROM products WHERE title LIKE '%ipod nano%' OR description LIKE '%ipod nano%' UNION SELECT products.id, 1 AS score FROM products WHERE title LIKE '%ipod%' OR title LIKE '%nano%' OR description LIKE '%ipod%' OR description LIKE '%nano%' ORDER BY score DESC ) AS T1;
  • 6. Наївне розв’язання Яке насправді більш схоже було на це: SELECT product_id FROM ( SELECT products.id AS product_id, 2 AS score FROM products WHERE title LIKE '%ipod nano%' OR description LIKE '%ipod nano%' UNION SELECT products.id, 1 AS score FROM products WHERE title LIKE '%ipod%' OR title LIKE '%nano%' OR description LIKE '%ipod%' OR description LIKE '%nano%' ORDER BY score DESC ) AS T1; І воно реально працювало на продакшні!
  • 7. Наївне розв’язання Спробуйте вгадати, які були проблеми у такого підходу 😉
  • 8. 1. Перформанс Довгі пошукові запити, багато продуктів, довгі описи продуктів - все приводило до деградації видайності. Проблеми наївного підходу
  • 9. 2. Все ще “погані” результати пошуку (принаймні, не перша сторінка) Проблеми наївного підходу
  • 10. Проблеми наївного підходу 3. Складніші кейси не підтримувались Помилки при наборі тексту (typos), варіації слів (cables ↔ cable), скорочення (S.T.A.L.K.E.R. ↔ stalker), вже не говорячи про синоніми та ін.
  • 11. Проблеми наївного підходу Поки воно працює - не чіпай його!
  • 12. Як працюють всі ці гугли? І ось, нещодавно, я відкрив для себе цілий світ науки про Information Retrieval
  • 13. Сьогодні в програмі Information Retrieval - досить обширний і глибокий розділ науки, тож я розкажу тільки про такі (цікаві?) теми: ● Як зберігати дані ● Як сортувати результати ● Нечіткий пошук
  • 14. Сьогодні в програмі Гуглити за такими запитами: ● Inverted index ● TF-IDF ● Vector-space model ● Levenshtein & Levenshtein-Damerau distance
  • 16. Документ Документ - це текст, який ми скормлюємо пошуковому двіжку разом з його айдішником (тексту), щоб пізніше зрозуміти що конкретно було знайдено. A complete numerical series is followed by an incomplete numerical series. You need to solve that incomplete numerical series in the same pattern in which the complete numerical series is given. оце все - документ
  • 17. Терм Терм - це одне слово, фраза або скорочення з документу. A complete numerical series is followed by an incomplete numerical series. You need to solve that incomplete numerical series in the same pattern in which the complete numerical series is given. Кожне з них - терм
  • 18. Індекс Індекс - це структура даних, в якій ми зберігаємо документи, для пошуку. { "numerical": 4, "series": 4, "is": 2, "incomplete": 2, "complete": 2, "in": 2, "the": 2, "need": 1, "a": 1, "you": 1, "followed": 1, "to": 1, ... }
  • 19. Нахіба нам індекс? Табличка в базі даних: 1 -> long long long text 2 -> shorter but still long text 3 -> extremely, unbelievably long text Inverted index: term -> 1, 3, 155, 20195 other_term -> 2, 3, 14, 29, 31, 20114 rare_term -> 19852, 30421
  • 20. Нахіба нам індекс? Табличка в базі даних: 1 -> long long long text 2 -> shorter but still long text 3 -> extremely, unbelievably long text Inverted index: term -> 1, 3, 155, 20195 other_term -> 2, 3, 14, 29, 31, 20114 rare_term -> 19852, 30421 Запит буде порівняно з кожним документом, по черзі.
  • 21. Для кожного терму з запиту можна досить швидко витягнути всі документи, які мають в собі цей терм. Нахіба нам індекс? Табличка в базі даних: 1 -> long long long text 2 -> shorter but still long text 3 -> extremely, unbelievably long text Inverted index: term -> 1, 3, 155, 20195 other_term -> 2, 3, 14, 29, 31, 20114 rare_term -> 19852, 30421
  • 22. Нахіба нам індекс? Пошук в Lucene Пошук в БД
  • 24. Нахіба нам індекс? Додавання і видалення даних в/з індексу повільне, але пошук - швидкий
  • 25. Inverted index: деякі реалізації Map<String, List<Long>> - кожному терму відповідає список айдішників документів, які мають цей терм. { "term1": ["doc1", "doc2"], "term2": ["doc2", "doc3"] }
  • 26. Map<String, Map<String, Long>> - кожному терму відповідає список айдішників документів і кількість цього терму в цьому документі. Розширена версія, (трошки) прискорює обчислення (див. далі) { "term1": { "doc1": 1, "doc2": 3 }, "term2": { "doc2": 4, "doc3": 1 } } Inverted index: деякі реалізації
  • 27. Matrix2D<Long> - двовимірний масив, кожному айдішнику документа (одна вісь матриці) відповідає кількість терма (друга вісь матриці) в цьому документі. Швидкі обчислення, але жере купу лишньої пам’яті. Inverted index: деякі реалізації doc1 doc2 doc3 term1 1 3 0 term2 0 4 1
  • 28. Inverted index: деякі реалізації Map<String, Map<String, List<Long>>> - терм - документ і позиції цього терма в цьому документі. Дозволяє шукати по фразах. { "term1": { "doc1": [0], "doc2": [0,2,6] }, "term2": { "doc2": [1,3,4,5], "doc3": [0] } }
  • 29. Створюємо inverted index 1. Розбиваємо текст на слова (отримаємо список слів; це називається “аналіз документу”) 2. Заповнюємо хешмапу індексу відповідно (як ключ беремо слово, як значення робимо або список ID документів, або кількість цього слова в документі, або шо) Для оптимізації, можна зліпити ці два кроки в один цикл/map-reduce.
  • 30. Пошук по індексу Дано: ● List<String> - наш пошуковий запит; слова які ТРЕБА або БАЖАНО щоб були у знайдених документах ● Index - індекс 😐
  • 31. const result = new Set(); for (let queryTerm of query) { const docs = index.get(queryTerm); for (let docId of docs) { result.add(docId); } } Пошук в індексі: шукаємо БУДЬ-ЯКИЙ терм
  • 32. Пошук в індексі: шукаємо БУДЬ-ЯКИЙ терм const result = []; for (let i = 0; i < query.length; ++i) { result = _.union(index.get(query[i])); }
  • 33. const result = new Set(index.get(query[0])); for (let i = 1; i < query.length; ++i) { const queryTerm = query[i]; const docs = index.get(queryTerm); for (let docId of result) { if (!docs.has(docId)) { result.delete(docId); } } } Пошук в індексі: шукаємо ВСІ терми
  • 34. Пошук в індексі: шукаємо ВСІ терми const result = index.get(query[0]); for (let i = 1; i < query.length; ++i) { result = _.intersection(index.get(query[i])); }
  • 35. Пошук: крок 2 Знайти документи не складно. Складно посортувати їх шоб було гарненько.
  • 36. TF-IDF TF - term frequency IDF - inverse document frequency
  • 37. Число, яке показує наскільки терм важливий для конкретного документу TF-IDF
  • 38. TF-IDF Нехай у нас є такі два тексти: A complete numerical series is followed by an incomplete numerical series. You need to solve that incomplete numerical series in the same pattern in which the complete numerical series is given. Farscape is my favourite series. It’s unique and, as far as i know, there is no similar series out there, so give it a try if you want to see something unique. Some say Firefly, Andromeda, Lexx and so on are similar. But those series are very different and share nothing in common with farscape in my opinion.
  • 39. Наскільки релевантне слово “series” для кожного з них? TF-IDF
  • 40. TF-IDF Вот еті рєбята: A complete numerical series is followed by an incomplete numerical series. You need to solve that incomplete numerical series in the same pattern in which the complete numerical series is given. Farscape is my favourite series. It’s unique and, as far as i know, there is no similar series out there, so give it a try if you want to see something unique. Some say Firefly, Andromeda, Lexx and so on are similar. But those series are very different and share nothing in common with farscape in my opinion.
  • 41. Примітка: TF-IDF простіше рахувати використовуючи розширені варіанти індексу, як от цей: // term -> { documentId -> number of occurrences } const index: Map<String, Map<Number, Number>>; // documentId -> [ term ] const documents: Map<Number, List<String>>; TF-IDF
  • 42. TF Глянем на аналіз цих двох текстів: { "series": 3, "and": 3, "there": 2, "unique": 2, "farscape": 2, "as": 2, "my": 2, "similar": 2, "so": 2, "is": 2, "are": 2, "in": 2, "favourite": 1 } { "numerical": 4, "series": 4, "is": 2, "incomplete": 2, "complete": 2, "in": 2, "the": 2, "need": 1, "a": 1, "you": 1, }
  • 43. TF tf = (term, docId) => index.get(term).get(docId) / documents.get(docId).length; Співвідношення кількості цього терма в документі до кількості всіх слів в документі.
  • 44. TF-IDF Індекс для кожного з цих текстів: A complete numerical series is followed by an incomplete numerical series. You need to solve that incomplete numerical series in the same pattern in which the complete numerical series is given. { "numerical": 4, "series": 4, "is": 2, "incomplete": 2, "complete": 2, "in": 2, "the": 2, "need": 1, "a": 1, "you": 1, }
  • 45. TF-IDF Farscape is my favourite series. It’s unique and, as far as i know, there is no similar series out there, so give it a try if you want to see something unique. Some say Firefly, Andromeda, Lexx and so on are similar. But those series are very different and share nothing in common with farscape in my opinion. { "series": 3, "and": 3, "there": 2, "unique": 2, "farscape": 2, "as": 2, "my": 2, "similar": 2, "so": 2, "is": 2, "are": 2, "in": 2, "favourite": 1 } Індекс для кожного з цих текстів:
  • 46. IDF А тепер зиркнемо на весь індекс: { "a": [1, 2], "series": [1, 2], "is": [1, 2], "you": [1, 2], "to": [1, 2], "in": [1, 2], "an": [1], "incomplete": [1], "opinion": [2], "need": [1], "numerical": [1], }
  • 47. IDF idf = (term) => Math.log(index.size / index.get(term).size) Логарифм від співвідношення кількості слів в індексі до кількості документів, в яких є даний терм.
  • 48. // term -> { documentId -> number of occurrences } let index: Map<String, Map<Number, Number>>; // documentId -> [ term ] let documents: Map<Number, List<String>>; tf = (term, docId) => index.get(term).get(docId) / documents.get(docId).length; idf = (term) => Math.log(1 + (index.size / index.get(term).size)); tf_idf = (term, docId) => tf(term, docId) * idf(term); TF-IDF
  • 49. tf_ifd("series", doc1) = 0.43886417827898777 tf_idf("series", doc2) = 0.17592400249976667 Тобто, слово “series” значно менш релевантне для другого документу TF-IDF
  • 50. tf_ifd("series", doc1) = 0.43886417827898777 tf_idf("series", doc2) = 0.17592400249976667 Тобто, слово “series” значно менш релевантне для другого документу ШТА? TF-IDF
  • 51. А проблеми роблять ось такі штуки: { "and": 3, "there": 2, "as": 2, "my": 2, "so": 2, "is": 2, "are": 2, "in": 2, "it’s": 1, "it": 1, "a": 1, "if": 1, "to": 1, "on": 1, "i": 1, "but": 1, "those": 1, "with": 1 } TF-IDF
  • 52. tf_ifd("series", doc1) = 0.6040849772288726 tf_idf("series", doc2) = 0.2517020738453635 TF-IDF: фільтруємо слова-паразити
  • 53. TF-IDF це канєшно круто, але воно працює тільки для одного слова і одного документа. А якшо ми робим гугль, і у нас ТИСЯЧІ, МІЛЬЙОНИ документів? Чутка математики
  • 54. Vector-space model Вумні дядьки взяли і використали вектори (по суті - просто масив чисел, з парою хитрих операцій).
  • 55. Vector-space model Але ці дядьки (як на мене) лажанулись, намагаючись пояснити цю штуку
  • 56. Vector-space model Але ці дядьки (як на мене) лажанулись, намагаючись пояснити цю штуку
  • 57. Vector-space model Але ці дядьки (як на мене) лажанулись, намагаючись пояснити цю штуку
  • 59. Vector-space model Якщо ми представимо кожен документ і запит як вектор, ми зможемо визначити наскільки вони схожі, взявши косинус кута між ними.
  • 60. Vector-space model Безполєзно малювати ще одну картінку, так шо хавай, піпл, табличку: term doc ID doc1 doc2 query series 1 1 1 you 1 1 0 numerical 1 0 0 andromeda 0 1 1 occurrences
  • 61. Vector-space model query = "series andromeda"; similarity(doc1, query) = cos((1, 1, 1, 0), (1, 0, 0, 1)) = 0.4 similarity(doc2, query) = cos((1, 1, 0, 1), (1, 0, 0, 1)) = 0.8 term doc ID doc1 doc2 query series 1 1 1 you 1 1 0 numerical 1 0 0 andromeda 0 1 1
  • 62. Vector-space model query = "numerical series"; similarity(doc1, query) = cos((1, 1, 1, 0), (1, 0, 1, 0)) = 0.8 similarity(doc2, query) = cos((1, 1, 0, 1), (1, 0, 1, 0)) = 0.4 term doc ID doc1 doc2 query series 1 1 1 you 1 1 0 numerical 1 0 1 andromeda 0 1 0
  • 63. Vector-space model query = "series numerical"; similarity(doc1, query) = cos((1, 1, 1, 0), (1, 0, 1, 0)) = 0.8 similarity(doc2, query) = cos((1, 1, 0, 1), (1, 0, 1, 0)) = 0.4 Проблема в тому, що перестановка слів в запиті не дає нікакого еффекту.
  • 64. Після всього вищесказаного, у вас має виникнути одне питання
  • 66. Ліпим все до купи class Index { private index: Map<String, Map<String, Number>>; // documentId -> [ term ] private documents: Map<String, Array<String>>; // documentId -> text private originals: Map<String, String>; ... }
  • 67. similarity(doc1: Array<String>, doc2: Array<String>): Number { let score = 0; for (const term of this.index.keys()) { score += this.tf(term, doc1) * this.tf(term, doc2); } return score; } Ліпим все до купи
  • 68. public search(query: String) { const queryTerms = this.parse(query); const scores = new Map(); const counts = new Map(); for (const term of queryTerms) { const matches = this.index.get(term); for (const docId of matches.keys()) { scores.set(docId, scores.get(docId) + this.similarity(queryTerms, this.documents.get(docId))); counts.set(docId, counts.get(docId) + 1); } } for (const docId of scores.keys()) { results.set(docId, scores.get(docId) * counts.get(docId)); } } Ліпим все до купи
  • 71. Бонус: did you mean: *that*?
  • 72. Нечіткий пошук Прикольна фіча, яка робить гугл “розумним” називається “нечіткий пошук” (fuzzy search) - коли алгоритм знаходить слова, подібні до слова з запиту (враховуючи всякі очєпяткі, синоніми і т.д.).
  • 73. Нечіткий пошук Простий і ефективний алгоритм який рахує наскільки два слова подібні між собою - відстань Левенштейна (з розширенням, відстань Левенштейна- Дамерау)
  • 74. Відстань Левенштейна Рахує скільки “операцій” треба зробити зі словом А щоб отримати слово Б.
  • 75. Відстань Левенштейна Ось ці операції: ● Додати один символ ● Видалити один символ ● Замінити один символ на інший
  • 76. Відстань Левенштейна perference -> preference (2 заміни) perfmance -> performance (2 додання) battlestart -> battlestar (1 видалення)
  • 77. levenshtein(s1, s2) { let v0 = []; let v1 = []; for (let i = 0; i <= s1.length; ++i) { v0[i] = i; } for (let i = 0; i < s2.length; ++i) { v1[0] = i + 1; for (let j = 0; j < s1.length; ++j) { v1[j + 1] = Math.min(v0[j + 1] + 1, v0[j] + 1, v0[j] + (s1[i] != s2[j])); } let tmp = v0; v0 = v1; v1 = tmp; } return v0[s1.length]; } Відстань Левенштейна
  • 78. Відстань Левенштейна-Дамерау Те ж саме, що і відстань Левенштейна, тільки рахує ще одну операцію: ● Додати один символ ● Видалити один символ ● Замінити один символ на інший ● Поміняти два сусідні символи місцями
  • 79. Відстань Левенштейна-Дамерау perference -> preference (1 перестановка) perfmance -> performance (2 додання) battlestart -> battlestar (1 видалення)
  • 80. levenshtein_damerau(s1, s2) { let v0 = []; let v1 = []; for (let i = 0; i <= s1.length; ++i) { v0[i] = i; } for (let i = 0; i < s2.length; ++i) { v1[0] = i + 1; for (let j = 0; j < s1.length; ++j) { v1[j + 1] = Math.min(v0[j + 1] + 1, v0[j] + 1, v0[j] + (s1[i] != s2[j] ? 1 : 0)); if (i > 0 && j > 0 && s1[i] == s2[j - 1] && s1[i - 1] == s2[j]) { v1[j + 1] = Math.min(v1[j + 1], v0[j - 1]); } } let tmp = v0; v0 = v1; v1 = tmp; } return v0[s1.length]; } Відстань Левенштейна-Дамерау