Статьи

Полнотекстовое индексирование Lucene с Neo4j

Я провел некоторое время, работая над полнотекстовым поиском Neo4j. Основные цели были следующими

    • Контролировать указатели индекса
    • Полнотекстовый поиск
    • Все операции выполняются через Отдых
    • Можно создать индекс при создании узла
    • Можно обновить и индексировать
    • Можно проверить, существует ли индекс
    • При загрузке Neo4j в облаке запускаются проверки индекса
    • Индекс запроса с использованием полнотекстового поиска языка запросов Lucene.

Скачать:

Это основано на Neo4jClient:

Исходный код на:

Вступление

Поэтому с вышеуказанными целями я решил перейти на ручную индексацию. Основная причина в том, что я могу поместить индекс, указывающий на узел A, на основе значений в узле B.

Представьте себе следующее.

У вас есть узел A со списком:

Фамилия, Имя и Отчество. Однако узел A также имеет отношение к узлу B, который имеет другие имена, например, отображаемые имена, имена аватаров и AKA.

Таким образом, при ручном индексировании вы можете иметь все перечисленные выше записи для имен в узле A и узле B, указывающих только на узел A.

Таким образом, в вызове Rest для сервера Neo4j это будет выглядеть примерно так в Fiddler.

образ

Обратите внимание на следующее:

URL: http: // localhost: 7474 / db / data / index / node / {IndexName} / {Key} / {Value }

Таким образом, если бы мы добавляли 3 имени для одного и того же клиента из 2 разных узлов. У вас будут одинаковые IndexName и Key с разными значениями в URL. Указатель узла (в теле запроса) будет тогда адресом для Узла.

Neo4jClient Nuget Package

Я обновил Neo4jClient, который находится на Nuget, чтобы теперь поддерживать:

  • Создание точных или полнотекстовых индексов самостоятельно, так что он просто существует
  • При создании индексов Exact или FullTest при создании узла ссылка на узел будет автоматически рассчитана.
  • Обновление индекса
  • Удаление записей из индекса.

    Диаграмма классов для решения по индексированию в Neo4jClient.

образ

RestSharp

Пакет Neo4jClient использует RestSharp, что делает все операции индексного вызова тривиальной задачей для нас, поэтому давайте посмотрим на часть кода внутри клиента, чтобы увидеть, как использовать индексный API вручную из .Net, а затем в следующем разделе. Посмотрите, как мы используем этот код из другого приложения.

public Dictionary<string, IndexMetaData> GetIndexes(IndexFor indexFor)
       {
           CheckRoot();

           string indexResource;
           switch (indexFor)
           {
               case IndexFor.Node:
                   indexResource = RootApiResponse.NodeIndex;
                   break;
               case IndexFor.Relationship:
                   indexResource = RootApiResponse.RelationshipIndex;
                   break;
               default:
                   throw new NotSupportedException(string.Format("GetIndexes does not support indexfor {0}", indexFor));
           }

           var request = new RestRequest(indexResource, Method.GET)
           {
               RequestFormat = DataFormat.Json,
               JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
           };

           var response =  client.Execute<Dictionary<string, IndexMetaData>>(request);

           if (response.StatusCode != HttpStatusCode.OK)
               throw new NotSupportedException(string.Format(
                   "Received an unexpected HTTP status when executing the request.\r\n\r\n\r\nThe response status was: {0} {1}",
                   (int)response.StatusCode,
                   response.StatusDescription));

           return response.Data;
       }

       public bool CheckIndexExists(string indexName, IndexFor indexFor)
       {
           CheckRoot();

           string indexResource;
           switch (indexFor)
           {
               case IndexFor.Node:
                   indexResource = RootApiResponse.NodeIndex;
                   break;
               case IndexFor.Relationship:
                   indexResource = RootApiResponse.RelationshipIndex;
                   break;
               default:
                   throw new NotSupportedException(string.Format("IndexExists does not support indexfor {0}", indexFor));
           }

           var request = new RestRequest(string.Format("{0}/{1}",indexResource, indexName), Method.GET)
           {
               RequestFormat = DataFormat.Json,
               JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
           };

           var response = client.Execute<Dictionary<string, IndexMetaData>>(request);

           return response.StatusCode == HttpStatusCode.OK;
       }

       void CheckRoot()
       {
           if (RootApiResponse == null)
               throw new InvalidOperationException(
                   "The graph client is not connected to the server. Call the Connect method first.");
       }

       public void CreateIndex(string indexName, IndexConfiguration config, IndexFor indexFor)
       {
           CheckRoot();

           string nodeResource;
           switch (indexFor)
           {
               case IndexFor.Node:
                   nodeResource = RootApiResponse.NodeIndex;
                   break;
               case IndexFor.Relationship:
                   nodeResource = RootApiResponse.RelationshipIndex;
                   break;
               default:
                   throw new NotSupportedException(string.Format("CreateIndex does not support indexfor {0}", indexFor));
           }

           var createIndexApiRequest = new
               {
                   name = indexName.ToLower(),
                   config
               };

           var request = new RestRequest(nodeResource, Method.POST)
               {
                   RequestFormat = DataFormat.Json,
                   JsonSerializer = new CustomJsonSerializer {NullHandling = JsonSerializerNullValueHandling}
               };
           request.AddBody(createIndexApiRequest);

           var response = client.Execute(request);

           if (response.StatusCode != HttpStatusCode.Created)
               throw new NotSupportedException(string.Format(
                   "Received an unexpected HTTP status when executing the request..\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                   indexName,
                   (int) response.StatusCode,
                   response.StatusDescription));
       }

       public void ReIndex(NodeReference node, IEnumerable<IndexEntry> indexEntries)
       {
           CheckRoot();

           var nodeAddress = string.Join("/", new[] {RootApiResponse.Node, node.Id.ToString()});

           var updates = indexEntries
               .SelectMany(
                   i => i.KeyValues,
                   (i, kv) => new {IndexName = i.Name, kv.Key, kv.Value});

           foreach (var update in updates)
           {
               if (update.Value == null)
                   break;

               string indexValue;
               if(update.Value is DateTimeOffset)
               {
                   indexValue = ((DateTimeOffset) update.Value).UtcTicks.ToString();
               }
               else if (update.Value is DateTime)
               {
                   indexValue = ((DateTime)update.Value).Ticks.ToString();
               }
               else
               {
                   indexValue = update.Value.ToString();
               }

               AddNodeToIndex(update.IndexName, update.Key, indexValue, nodeAddress);
           }
       }

       public void DeleteIndex(string indexName, IndexFor indexFor)
       {
           CheckRoot();

           string indexResource;
           switch (indexFor)
           {
               case IndexFor.Node:
                   indexResource = RootApiResponse.NodeIndex;
                   break;
               case IndexFor.Relationship:
                   indexResource = RootApiResponse.RelationshipIndex;
                   break;
               default:
                   throw new NotSupportedException(string.Format("DeleteIndex does not support indexfor {0}", indexFor));
           }

           var request = new RestRequest(string.Format("{0}/{1}", indexResource, indexName), Method.DELETE)
           {
               RequestFormat = DataFormat.Json,
               JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
           };

           var response = client.Execute(request);

           if (response.StatusCode != HttpStatusCode.NoContent)
               throw new NotSupportedException(string.Format(
                   "Received an unexpected HTTP status when executing the request.\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                   indexName,
                   (int)response.StatusCode,
                   response.StatusDescription));
       }

       void AddNodeToIndex(string indexName, string indexKey, string indexValue, string nodeAddress)
       {
           var nodeIndexAddress = string.Join("/", new[] { RootApiResponse.NodeIndex, indexName, indexKey, indexValue });
           var request = new RestRequest(nodeIndexAddress, Method.POST)
           {
               RequestFormat = DataFormat.Json,
               JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
           };
           request.AddBody(string.Join("", client.BaseUrl, nodeAddress));

           var response = client.Execute(request);

           if (response.StatusCode != HttpStatusCode.Created)
               throw new NotSupportedException(string.Format(
                   "Received an unexpected HTTP status when executing the request.\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                   indexName,
                   (int)response.StatusCode,
                   response.StatusDescription));
       }

       public IEnumerable<Node<TNode>> QueryIndex<TNode>(string indexName, IndexFor indexFor, string query)
       {
           CheckRoot();

           string indexResource;

           switch (indexFor)
           {
               case IndexFor.Node:
                   indexResource = RootApiResponse.NodeIndex;
                   break;
               case IndexFor.Relationship:
                   indexResource = RootApiResponse.RelationshipIndex;
                   break;
               default:
                   throw new NotSupportedException(string.Format("QueryIndex does not support indexfor {0}", indexFor));
           }

           var request = new RestRequest(indexResource + "/" + indexName, Method.GET)
               {
                   RequestFormat = DataFormat.Json,
                   JsonSerializer = new CustomJsonSerializer {NullHandling = JsonSerializerNullValueHandling}
               };

           request.AddParameter("query", query);

           var response = client.Execute<List<NodeApiResponse<TNode>>>(request);

           if (response.StatusCode != HttpStatusCode.OK)
               throw new NotSupportedException(string.Format(
                   "Received an unexpected HTTP status when executing the request.\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                   indexName,
                   (int) response.StatusCode,
                   response.StatusDescription));

           return response.Data == null
          ? Enumerable.Empty<Node<TNode>>()
          : response.Data.Select(r => r.ToNode(this));
       }

 

Использование Neo4jClient из приложения

Создайте индекс и проверьте, существует ли он

Это полезно при начальной загрузке Neo4j, чтобы увидеть, есть ли какие-либо индексы, которые ДОЛЖНЫ быть и отсутствуют, чтобы вы могли перечислить все узлы для этого индекса и добавить записи.

public void CreateIndexesForAgencyClients()
        {
            var agencies = graphClient
                .RootNode
                .Out<Agency>(Hosts.TypeKey)
                .ToList();

            foreach (var agency in agencies)
            {
                var indexName = IndexNames.Clients(agency.Data);
                var indexConfiguration = new IndexConfiguration
                    {
                        Provider = IndexProvider.lucene,
                        Type = IndexType.fulltext
                    };

                if (!graphClient.CheckIndexExists(indexName, IndexFor.Node))
                {
                    Trace.TraceInformation("CreateIndexIfNotExists {0} for Agency Key {0}", indexName, agency.Data.Key);
                    graphClient.CreateIndex(indexName, indexConfiguration, IndexFor.Node);
                    PopulateAgencyClientIndex(agency.Data);
                }
            }
        }

Создание записи узла индекса при создании узла

var indexEntries = GetIndexEntries(agency.Data, client, clientViewModel.AlsoKnownAses);

var clientNodeReference = graphClient.Create(
                client,
                new[] {new ClientBelongsTo(agencyNode.Reference)}, indexEntries);

public IEnumerable<IndexEntry> GetIndexEntries(Agency agency, Client client, IEnumerable<AlsoKnownAs> alsoKnownAses)
        {
            var indexKeyValues = new List<KeyValuePair<string, object>>
            {
                new KeyValuePair<string, object>(AgencyClientIndexKeys.Gender.ToString(), client.Gender)
            };

            if (client.DateOfBirth.HasValue)
            {
                var dateOfBirthUtcTicks = client.DateOfBirth.Value.UtcTicks;
                indexKeyValues.Add(new KeyValuePair<string, object>(AgencyClientIndexKeys.DateOfBirth.ToString(), dateOfBirthUtcTicks));
            }

            var names = new List<string>
            {
                client.GivenName,
                client.FamilyName,
                client.PreferredName,
            };

            if (alsoKnownAses != null)
            {
                names.AddRange(alsoKnownAses.Where(a => !string.IsNullOrEmpty(a.Name)).Select(aka => aka.Name));
            }

            indexKeyValues.AddRange(names.Select(name => new KeyValuePair<string, object>(AgencyClientIndexKeys.Name.ToString(), name)));

            return new[]
            {
                new IndexEntry
                {
                    Name = IndexNames.Clients(agency),
                    KeyValues = indexKeyValues.Where(v => v.Value != null)
                }
            };
        }
		

Переиндексировать узел

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

void PopulateAgencyClientIndex(Agency agency)
        {
            var clients = graphClient
                .RootNode
                .Out<Agency>(Hosts.TypeKey, a => a.Key == agency.Key)
                .In<Client>(ClientBelongsTo.TypeKey);

            foreach (var client in clients)
            {
                var clientService = clientServiceCallback();
                var akas = client.Out<AlsoKnownAs>(IsAlsoKnownAs.TypeKey).Select(a => a.Data);
                var indexEntries = clientService.GetIndexEntries(agency, client.Data, akas);
                graphClient.ReIndex(client.Reference, indexEntries);
            }
        }
		

Запрос индекса полнотекстового поиска с помощью Lucene

Ниже приведен пример кода для запроса полнотекстового поиска. В основном ваши записи индекса для человека с

Имя: Боб, Фамилия: Ван де Билдер, Ака1: Бобби, Ака2: Бобс, ПредпочтительныйИмя: Боб Строитель

Записи индекса должны будут выглядеть как

Ключ:
Имя значения: Имя Боба:
Имя Ван
:
Имя де :
Имя строителя :
Имя Бобби : Бобс

Помните, что в Lucene есть анализатор пробелов, поэтому любые имена с пробелами ДОЛЖНЫ становиться новой записью индекса, поэтому мы выделяем имена на основе пробелов, и это становится нашей коллекцией IndexEntries. Выше относится к контексту полнотекстового поиска.

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

Имя: Боб Строитель

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

Давайте посмотрим на пример запроса индекса.

[Test]
public void VerifyWhenANewClientIsCreateThatPartialNameCanBeFuzzySearchedInTheFullTextSearchIndex()
{
    using (var agency = Data.NewTestAgency())
    using (var client = Data.NewTestClient(agency, c =>
    {
        c.Gender = Gender.Male;
        c.GivenName = "Joseph";
        c.MiddleNames = "Mark";
        c.FamilyName = "Kitson";
        c.PreferredName = "Joey";

        c.AlsoKnownAses = new List<AlsoKnownAs>
            {
               new AlsoKnownAs {Name = "J-Man"},
               new AlsoKnownAs {Name = "J-Town"}
            };
    }
        ))
    {
        var indexName = IndexNames.Clients(agency.Agency.Data);
        const string partialName = "+Name:Joe~+Name:Kitson~";
        var result = GraphClient.QueryIndex<Client>(indexName, IndexFor.Node, partialName);
        Assert.AreEqual(client.Client.Data.UniqueId, result.First().Data.UniqueId);
    }
}

Даты

Обратите внимание, что в некотором коде вы, возможно, заметили, что когда я сохраняю записи дат в индексе, я сохраняю их как тики, так что это будут длинные числа, это потрясающе, так как дает грубую силу для поиска дат по длинным Улыбка

[Test]
       public void VerifyWhenANewClientIsCreateThatTheDateOfBirthCanBeRangeSearchedInTheFullTextSearchIndex()
       {
           // Arrange
           const long dateOfBirthTicks = 634493518171556320;
           using (var agency = Data.NewTestAgency())
           using (var client = Data.NewTestClient(agency, c =>
           {
               c.Gender = Gender.Male;
               c.GivenName = "Joseph";
               c.MiddleNames = "Mark";
               c.FamilyName = "Kitson";
               c.PreferredName = "Joey";
               c.DateOfBirth = new DateTimeOffset(dateOfBirthTicks, new TimeSpan());
               c.CurrentAge = null;
               c.AlsoKnownAses = new List<AlsoKnownAs>
                   {
                      new AlsoKnownAs {Name = "J-Man"},
                      new AlsoKnownAs {Name = "J-Town"}
                   };
           }
               ))
           {
               // Act
               var indexName = IndexNames.Clients(agency.Agency.Data);
               var partialName = string.Format("DateOfBirth:[{0} TO {1}]", dateOfBirthTicks - 5, dateOfBirthTicks + 5);
               var result = GraphClient.QueryIndex<Client>(indexName, IndexFor.Node, partialName);
               // Assert
               Assert.AreEqual(client.Client.Data.UniqueId, result.First().Data.UniqueId);
           }
       }
	

Резюме

Ну, я надеюсь, что вы нашли этот пост полезным. Neo4jClientis на nuget, так что пользуйтесь bash и хотел бы узнать ваши отзывы.

Скачать


NuGetPackage:

Исходный код на:

ура