@vadamlyuk

Ранжирование sphinx: как поднять в топ выдачи точные совпадения?

Добрый день,
наверное задам этот вопрос в 100500 раз, проведя в тестах и поиске гугл я так и не нашел для себя внятного ответа.

Постановка задачи:
Есть поисковое слово, к примеру "тест", нужно найти следующие его словоформы:
- тест
- тест*
- *тест*
При этом нужно отранжировать результаты:
- сначала по словоформе (т.е. документы с четкими совпадениями тест должны стоять выше чем документы с совпадениями тест*)
- если словоформы одинаковые, то выше идет документ, у кого больше словоформ этого типа (т.е. документ "тест тест" должен стоять выше чем документ "просто тест")

Положение слова в документе, его частотность и пр. не важны, т.е. слова "это" и "контрвзбзднуть" для меня равны

Тестовый индекс:
index rttest2
{
	type	= rt
	path	= /var/sphinx/rttest2
	rt_attr_string = phrase1
	rt_field = phrase2
	min_infix_len = 2
	index_exact_words = 1
}


INSERT into rttest2(id,phrase1,phrase2) values(1,'очень простой тест','очень простой тест');
INSERT into rttest2(id,phrase1,phrase2) values(2,'хитрый тестик','хитрый тестик');
INSERT into rttest2(id,phrase1,phrase2) values(3,'супертестик','супертестик');
INSERT into rttest2(id,phrase1,phrase2) values(4,'тестировщик тестов','тестировщик тестов');


Неправильное решение 1: Используем OPTION ranker=sph04 (это типичный ответ, если спрашивают про точные совпадения)
... потому что это ранжирование учитывает прежде всего положение слова в документе и частотность:

select *,weight() from rttest2 where match('тест|тест*|*тест*') and id <= 4 option ranker=sph04;
+------+-------------------------------------+----------+
| id   | phrase1                             | weight() |
+------+-------------------------------------+----------+
|    3 | супертестик                         |     6430 |
|    4 | тестировщик тестов                  |     6310 |
|    1 | очень простой тест                  |     4430 |
|    2 | хитрый тестик                       |     4362 |
+------+-------------------------------------+----------+


Не очень хорошее решение 2: пишем собственное ранжирование на основе hit_count и exact_order
... потому что это работает при одном поисковом слове, а при болшем количестве - нет

select *,weight() from rttest2 where match('тест|тест*|*тест*') option ranker=expr('sum(exact_order*10+hit_count)');

+------+-------------------------------------+----------+
| id   | phrase1                             | weight() |
+------+-------------------------------------+----------+
|    1 | очень простой тест                  |       13 |
|    4 | тестировщик тестов                  |        4 |
|    2 | хитрый тестик                       |        2 |
|    3 | супертестик                         |        1 |
+------+-------------------------------------+----------+


Однако, добавим еще один документ и второе слово в поиск и все становится плохо (так как exact_order перестает работать):

INSERT into rttest2(id,phrase1,phrase2) values(5,'тестик тестик простой','тестик тестик простой');
select *,weight() from rttest2 where match('(тест|тест*|*тест*)&(простой|простой*|*простой*)') option ranker=expr('sum(exact_order*10+hit_count)');

+------+------------------------------------------+----------+
| id   | phrase1                                  | weight() |
+------+------------------------------------------+----------+
|    5 | тестик тестик простой                    |        7 |
|    1 | очень простой тест                       |        6 |
+------+------------------------------------------+----------+


Непонятно почему не работающее решение 3: пытаемся добавить вес словоформам путем повторения (чтоб не использовать exact_order), но ранжирование вообще перестает работать, так как hit_count становится всегда равным 1:

select *,weight() from rttest2 where match('тест|тест*|*тест*') option ranker=expr('sum(hit_count)');
+------+------------------------------------------+----------+
| id   | phrase1                                  | weight() |
+------+------------------------------------------+----------+
|    4 | тестировщик тестов                       |        4 |
|    5 | тестик тестик простой                    |        4 |
|    1 | очень простой тест                       |        3 |
|    2 | хитрый тестик                            |        2 |
|    3 | супертестик                              |        1 |
+------+------------------------------------------+----------+


select *,weight() from rttest2 where match('тест|тест|тест*|*тест*') option ranker=expr('sum(hit_count)');
+------+------------------------------------------+----------+
| id   | phrase1                                  | weight() |
+------+------------------------------------------+----------+
|    4 | тестировщик тестов                       |        2 |
|    5 | тестик тестик простой                    |        2 |
|    1 | очень простой тест                       |        1 |
|    2 | хитрый тестик                            |        1 |
|    3 | супертестик                              |        1 |
+------+------------------------------------------+----------+


вывод с packedfactors не привожу, так как он делает сообщение не читаемым, желающие могут легко включить его в запросы

Подозреваю, что тут есть какая-то ошибка или "фича" sphinx, которая мешает воспользоваться этим трюком.

Подитоживая: нормального решения я не нашел, есть приемлеммое решение для одного поискового слова, но нет решения для нескольких поисковых форм. Возможно кто-то придложет решение получше
  • Вопрос задан
  • 1827 просмотров
Решения вопроса 1
@vadamlyuk Автор вопроса
UPD. Сам отвечаю на свой вопрос:
1. Если использовать расширенный язык запрос, то там есть такое понятие, как "IDF booster", что собственно и есть вес поискового слова (к сожалению, в ranker expression этот чистый вес использовать нельзя, можно только взять в связке с IDF. для моего случая это не важно, но ниже объяснию когда это может быть проблемой)
Веса выставляются после каждого слова через знак ^:

... where match('(тест^100|тест*^5|*тест*)') ...

2. Пишем собственное ranker expression (формулу, по которой производится ранжирование) и считаем в ней сумму tf_idf (которое является произведением tf - количества раз, которое встречается поисковое слово в запросе и idf (которое в свою очередь является произведением веса слова, заданного вами в запросе на некий коэффициент)). Важно отметить, что для тест* и *тест* почему-то имеет отрицательное значение (хотя в документации сказано, что idf без учета веса может принимать значения от 0 до 1), по этому в нашем случае берем его абсолютное значение и полностью запрос будет выглядеть так:

select *,weight() from rttest2 where match('(тест^100|тест*^5|*тест*)') option ranker=expr('sum(abs(tf_idf)*1000)');

+------+------------------------------------------+----------+
| id   | phrase1                                  | weight() |
+------+------------------------------------------+----------+
|    1 | очень простой тест                       |    14423 |
|    4 | тестировщик тестов                       |     1846 |
|    5 | тестик тестик простой                    |     1846 |
|    2 | хитрый тестик                            |      923 |
|    3 | супертестик                              |      155 |
+------+------------------------------------------+----------+


Теперь обещанная сноска по поводу IDF:
Согласно этому объяснению автора:
IDF это очередная магическая метрика, которая учитывает «редкость» слова: для суперчастых слов (которые есть в каждом документе) она равна 0, а для уникального слова (1 вхождение в 1 документ во всей коллекции) равна 1.

Т.е. для словоформ одного слова со звездочками и без значения IDF будут равны,
т.е. для слов тест, тест* и *тест* IDF будет практически прямо пропорционален весу, который вы задали в запросе

Но если Вы хотите проделать этот же трюк со словами "это" и "контрвзбзднуть" в варианте запроса "или или", то скорей всего у Вас ничего не получится, так как IDF будет сильно зависить от набора документов, которые у Вас содержаться в индексе. По этому конечно было бы классно, если можно было задавать вес просто конкретному слову, без привязки к idf
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
opium
@opium
Просто люблю качественно работать
Ищите отдельно потом сделайте слияние айдишников найденных с ранжированием нужны.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы