Эффективный семантический поиск по неструктурированному тексту в Neo4j

Эффективный семантический поиск в Neo4j

Интегрируйте вновь добавленный векторный индекс в LangChain, чтобы улучшить ваши приложения на основе RAG

С момента появления ChatGPT полгода назад технологический ландшафт претерпел трансформационное изменение. Исключительная способность ChatGPT к обобщению снизила требования к специализированным командам глубокого обучения и обширным обучающим наборам данных для создания пользовательских моделей NLP. Это демократизировало доступ к различным NLP-задачам, таким как суммирование и извлечение информации, делая их более доступными, чем когда-либо раньше. Однако вскоре мы поняли ограничения моделей типа ChatGPT, таких как ограничение на дату знаний и отсутствие доступа к личной информации. По моему мнению, за этим последовала вторая волна преобразования генеративного ИИ с появлением приложений Retrieval Augmented Generation (RAG), где вы подаете модели соответствующую информацию во время запроса, чтобы создавать более точные и точные ответы.

Поток приложения RAG. Изображение автора. Иконки из https://www.flaticon.com/

Как уже упоминалось, приложения RAG требуют интеллектуального инструмента поиска, который способен извлекать дополнительную информацию на основе ввода пользователя, что позволяет LLM-моделям производить более точные и актуальные ответы. Сначала основное внимание уделялось извлечению информации из неструктурированного текста с использованием семантического поиска. Однако вскоре стало очевидно, что комбинация структурированных и неструктурированных данных является лучшим подходом к приложениям RAG, если вы хотите выйти за пределы приложений “Чат с вашим PDF”.

Neo4j был и остается отличным вариантом для работы с структурированной информацией, но он немного затруднялся с семантическим поиском из-за своего грубой силы подхода. Однако борьба осталась в прошлом, поскольку Neo4j в версии 5.11 представил новый векторный индекс, который эффективно выполняет семантический поиск в неструктурированном тексте или других модальностях встроенных данных. Вновь добавленный векторный индекс делает Neo4j отличным выбором для большинства приложений RAG, поскольку теперь он прекрасно работает как с структурированными, так и с неструктурированными данными.

В этой статье блога я покажу вам, как настроить векторный индекс в Neo4j и интегрировать его в экосистему LangChain. Код доступен на GitHub.

Настройка среды Neo4j

Вам необходимо настроить Neo4j 5.11 или более позднюю версию, чтобы следовать примерам в этой статье блога. Самый простой способ – запустить бесплатный экземпляр на Neo4j Aura, который предлагает облачные экземпляры базы данных Neo4j. Кроме того, вы также можете настроить локальный экземпляр базы данных Neo4j, загрузив приложение Neo4j Desktop и создав локальный экземпляр базы данных.

После создания базы данных Neo4j вы можете использовать библиотеку LangChain для подключения к ней.

from langchain.graphs import Neo4jGraphNEO4J_URI="neo4j+s://1234.databases.neo4j.io"NEO4J_USERNAME="neo4j"NEO4J_PASSWORD="-"graph = Neo4jGraph(    url=NEO4J_URI,    username=NEO4J_USERNAME,    password=NEO4J_PASSWORD)

Настройка векторного индекса

Векторный индекс Neo4j работает на основе Lucene, где Lucene реализует иерархический навигационный граф Small World (HNSW) для выполнения приближенного поиска ближайших соседей (ANN) в векторном пространстве.

Реализация векторного индекса Neo4j предназначена для индексации одного свойства узла с меткой узла. Например, если вы хотите проиндексировать узлы с меткой Chunk на их свойство узла embedding, вы должны использовать следующую процедуру Cypher.

CALL db.index.vector.createNodeIndex(  'wikipedia', // имя индекса  'Chunk',     // метка узла  'embedding', // свойство узла   1536,       // размер вектора   'cosine'    // метрика сходства)

Вместе с именем индекса, меткой узла и свойством вы должны указать размер вектора (размер вложения) и метрику сходства. Мы будем использовать модель вложения OpenAI text-embedding-ada-002, которая использует размер вектора 1536 для представления текста в пространстве вложений. В настоящее время доступны только метрики сходства cosine и Евклидово расстояние. OpenAI рекомендует использовать метрику сходства косинуса при использовании их модели вложения.

Заполнение индекса вектора

Neo4j является безсхемной по своей природе, что означает, что она не накладывает никаких ограничений на то, что может быть в свойстве узла. Например, свойство embedding узла Chunk может содержать целые числа, список целых чисел или даже строки. Давайте попробуем это.

WITH [1, [1,2,3], ["2","5"], [x in range(0, 1535) | toFloat(x)]] AS exampleValuesUNWIND range(0, size(exampleValues) - 1) as indexCREATE (:Chunk {embedding: exampleValues[index], index: index})

Этот запрос создает узел Chunk для каждого элемента в списке и использует элемент в качестве значения свойства embedding. Например, у первого узла Chunk значение свойства embedding будет 1, у второго узла [1,2,3] и так далее. Neo4j не накладывает никаких правил на то, что можно хранить в свойствах узлов. Однако, у индекса вектора есть четкие инструкции о типе значений и их размерности вектора, которые он должен индексировать.

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

CALL db.index.vector.queryNodes(  'wikipedia', // имя индекса   3, // количество соседей для возврата   [x in range(0,1535) | toFloat(x) / 2] // входной вектор)YIELD node, scoreRETURN node.index AS index, score

Если вы выполните этот запрос, вы получите только один узел, даже если запросили верхние 3 соседей, чтобы они были возвращены. Почему так? Индекс вектора индексирует только значения свойств, где значение является списком чисел с плавающей запятой с указанным размером. В этом примере только одно значение свойства embedding имело тип списка чисел с плавающей запятой с выбранной длиной 1536.

Узел проиндексирован векторным индексом, если выполняются все следующие условия:

  • Узел содержит настроенную метку.
  • Узел содержит настроенный ключ свойства.
  • Соответствующее значение свойства имеет тип LIST<FLOAT>.
  • Размер соответствующего значения равен настроенной размерности.
  • Значение является допустимым вектором для настроенной функции сходства.

Интеграция индекса вектора в экосистему LangChain

Теперь мы реализуем простой пользовательский класс LangChain, который будет использовать индекс вектора Neo4j для получения актуальной и точной информации для генерации ответов. Но сначала нам нужно заполнить индекс вектора.

Поток данных с использованием индекса вектора Neo4j в приложениях RAG. Изображение автора. Иконки от flaticons.

Задача будет состоять из следующих шагов:

  • Получить статью из Википедии
  • Разделить текст на части
  • Сохранить текст вместе с его векторным представлением в Neo4j
  • Реализовать пользовательский класс LangChain для поддержки приложений RAG

В этом примере мы получим только одну статью из Википедии. Я решил использовать страницу Baldur’s Gate 3.

import wikipediabg3 = wikipedia.page(pageid=60979422)

Затем нам нужно разделить текст на части и вставить его векторное представление. Мы разделим текст по разделам, используя разделитель двойного нового абзаца, а затем воспользуемся моделью вложений OpenAI, чтобы представить каждый раздел соответствующим вектором.

import osfrom langchain.embeddings import OpenAIEmbeddingsos.environ["OPENAI_API_KEY"] = "API_KEY"embeddings = OpenAIEmbeddings()chunks = [{'text':el, 'embedding': embeddings.embed_query(el)} for                  el in bg3.content.split("\n\n") if len(el) > 50]

Прежде чем перейти к классу LangChain, нам нужно импортировать текстовые части в Neo4j.

graph.query("""UNWIND $data AS rowCREATE (c:Chunk {text: row.text})WITH c, rowCALL db.create.setVectorProperty(c, 'embedding', row.embedding)YIELD nodeRETURN distinct 'done'""", {'data': chunks})

Одна из вещей, которую вы можете заметить, это то, что я использовал процедуру db.create.setVectorProperty для сохранения векторов в Neo4j. Эта процедура используется для проверки того, что значение свойства действительно является списком чисел с плавающей запятой. Кроме того, она имеет дополнительное преимущество в виде сокращения объема хранилища свойства вектора примерно на 50%. Поэтому рекомендуется всегда использовать эту процедуру для сохранения векторов в Neo4j.

Теперь мы можем реализовать пользовательский класс LangChain, который используется для извлечения информации из векторного индекса Neo4j и использования ее для генерации ответов. Сначала мы определим выражение Cypher, используемое для извлечения информации.

vector_search = """WITH $embedding AS eCALL db.index.vector.queryNodes('wikipedia',3, e) yield node, scoreRETURN node.text AS resultORDER BY score DESCLIMIT 3"""

Как видите, я захардкодил имя индекса и количество соседей для извлечения. Вы можете сделать это динамическим, добавив соответствующие параметры, если хотите.

Пользовательский класс LangChain реализуется довольно просто.

class Neo4jVectorChain(Chain):    """Цепочка для вопросно-ответного взаимодействия с векторным индексом Neo4j."""    graph: Neo4jGraph = Field(exclude=True)    input_key: str = "query"  #: :meta private:    output_key: str = "result"  #: :meta private:    embeddings: OpenAIEmbeddings = OpenAIEmbeddings()    qa_chain: LLMChain = LLMChain(llm=ChatOpenAI(temperature=0), prompt=CHAT_PROMPT)    def _call(self, inputs: Dict[str, str], run_manager) -> Dict[str, Any]:        """Встроить вопрос и выполнить векторный поиск."""        question = inputs[self.input_key]                # Встроить вопрос        embedding = self.embeddings.embed_query(question)                # Извлечь соответствующую информацию из векторного индекса        context = self.graph.query(            vector_search, {'embedding': embedding})        context = [el['result'] for el in context]                # Сгенерировать ответ        result = self.qa_chain(            {"question": question, "context": context},        )        final_result = result[self.qa_chain.output_key]        return {self.output_key: final_result}

Я опустил некоторый шаблонный код, чтобы сделать его более читаемым. В основном, когда вы вызываете Neo4jVectorChain, выполняются следующие шаги:

  1. Встроить вопрос с использованием соответствующей модели встроек
  2. Использовать значение встроенного текста для извлечения наиболее похожего контента из векторного индекса
  3. Использовать предоставленный контекст из похожего контента для генерации ответа

Теперь мы можем протестировать нашу реализацию.

vector_qa = Neo4jVectorChain(graph=graph, embeddings=embeddings, verbose=True)vector_qa.run("What is the gameplay of Baldur's Gate 3 like?")

Ответ

Сгенерированный ответ. Изображение автора.

Используя опцию verbose, вы также можете оценить контекст, полученный из векторного индекса, который использовался для генерации ответа.

Резюме

Используя новые возможности индексирования векторов Neo4j, вы можете создать единый источник данных, который эффективно обеспечивает работу приложений, основанных на извлечении с улучшенным поколением. Это позволяет вам не только реализовать решения “Чат с вашим PDF или документацией”, но и проводить аналитику в режиме реального времени, все из одного надежного источника данных. Эта универсальная утилита может оптимизировать ваши операции и улучшить синергию данных, делая Neo4j отличным решением для управления структурированными и неструктурированными данными.

Как всегда, код доступен на GitHub.