Напомним, что набор данных FreeDB составляет 3,32 миллиона записей. Содержит большинство альбомов, которые вышли за последние несколько десятилетий. Для этого мы создали следующую базу данных Voron:
1: _storageEnvironment = new StorageEnvironment(StorageEnvironmentOptions.ForPath("FreeDB")); 2: using (var tx = _storageEnvironment.NewTransaction(TransactionFlags.ReadWrite)) 3: { 4: _storageEnvironment.CreateTree(tx, "albums"); 5: _storageEnvironment.CreateTree(tx, "ix_diskids"); 6: _storageEnvironment.CreateTree(tx, "ix_artists"); 7: _storageEnvironment.CreateTree(tx, "ix_titles"); 8: tx.Commit(); 9: }
Дерево альбомов содержит актуальную информацию об альбоме в виде строки json. И деревья ix_ * содержат обратные ссылки на него. Они наши индексы. Что бы это ни стоило, вы можете заметить, что именно так большинство СУБД реализует свою индексацию. Сейчас мы работаем на довольно низком уровне.
Также обратите внимание, что нам нужно определять эти деревья всякий раз, когда мы запускаем базу данных.
Теперь давайте сделаем несколько запросов, не так ли?
Мы собираемся начать с самого простого варианта, учитывая идентификатор диска, и дать мне соответствующий диск. Поскольку идентификаторы дисков практически уникальны, мы можем возвращать несколько результатов.
Отметим , что размер БД теперь составляет 9,00 ГБ.
Давайте посмотрим, как мы запрашиваем это. Мне больно писать его, но я создал «подобный хранилищу» интерфейс, потому что Ворон слишком низкий уровень, чтобы мы могли подвергнуть его воздействию пользовательского кода. На самом деле это одно из немногих мест, где интерфейс, подобный интерфейсу хранилища, хорош. Потому что скрывает дополнительную сложность и жесткость конструкции оправдана.
Интерфейс выглядит так:
Теперь давайте посмотрим, как мы на самом деле реализуем этого парня. Мы начнем с самого простого, выполняя поиск по идентификатору диска, который является почти уникальным значением, идентифицирующим диск.
1: public IEnumerable<Disk> FindByDiskId(string diskId) 2: { 3: using (var tx = _storageEnvironment.NewTransaction(TransactionFlags.Read)) 4: { 5: var dix = tx.GetTree("ix_diskids"); 6: var albums = tx.GetTree("albums"); 7: 8: using (var it = dix.MultiRead(tx, diskId)) 9: { 10: if (it.Seek(Slice.BeforeAllKeys) == false) 11: yield break; 12: do 13: { 14: var readResult = albums.Read(tx, it.CurrentKey); 15: using (var stream = readResult.Reader.AsStream()) 16: { 17: yield return _serializer.Deserialize<Disk>(new JsonTextReader(new StreamReader(stream))); 18: } 19: } while (it.MoveNext()); 20: } 21: 22: tx.Commit(); 23: } 24: }
Давайте рассмотрим этот код подробно. Мы используем транзакцию чтения, потому что мы не делаем никаких записей.
Здесь мы используем два дерева: ix_diskids, который является индексом на дисках, и дерево альбомов, которое содержит фактические данные.
В строке 8 мы делаем мульти чтение. Это сделано потому, что одно значение идентификатора диска может принадлежать нескольким альбомам.
Строки 10 и 11 нужны в случае отсутствия результатов. И линия 14 — то, где важная вещь случается. Результатом MultiRead является итератор, который содержит все идентификаторы альбомов с этим идентификатором диска. Затем мы читаем его из дерева альбомов, десериализуем и передаем пользователю. Все довольно просто.
Теперь давайте перейдем к более сложному сценарию, где мы хотим выполнить поиск по исполнителю или названию альбома.
1: public IEnumerable<Disk> FindByArtist(string prefix) 2: { 3: return FindByMultiValueIterator(prefix, "ix_artists"); 4: } 5: 6: public IEnumerable<Disk> FindByAlbumTitle(string prefix) 7: { 8: return FindByMultiValueIterator(prefix, "ix_titles"); 9: } 10: 11: private IEnumerable<Disk> FindByMultiValueIterator(string prefix, string treeIndexName) 12: { 13: using (var tx = _storageEnvironment.NewTransaction(TransactionFlags.Read)) 14: { 15: var dix = tx.GetTree(treeIndexName); 16: var albums = tx.GetTree("albums"); 17: 18: using (var multiValueIterator = dix.Iterate(tx)) 19: { 20: multiValueIterator.RequiredPrefix = prefix.ToLower(); 21: if (multiValueIterator.Seek(multiValueIterator.RequiredPrefix) == false) 22: yield break; 23: do 24: { 25: using (var albumsIterator = multiValueIterator.CreateMutliValueIterator()) 26: { 27: if (albumsIterator.Seek(Slice.BeforeAllKeys) == false) 28: continue; 29: do 30: { 31: var readResult = albums.Read(tx, albumsIterator.CurrentKey); 32: using (var stream = readResult.Reader.AsStream()) 33: { 34: yield return _serializer.Deserialize<Disk>(new JsonTextReader(new StreamReader(stream))); 35: } 36: } while (albumsIterator.MoveNext()); 37: } 38: } while (multiValueIterator.MoveNext()); 39: } 40: 41: tx.Commit(); 42: } 43: }
Вы можете видеть, что в обоих случаях мы обрабатываем это одинаково, потому что фактическое поведение одинаково. Мы не хотим делать точное совпадение. Если бы мы этого хотели, мы могли бы использовать ту же логику, что и в FindByDiskId. Но мы хотим сделать что-то большее, мы хотим иметь возможность поиска по префиксу , а не только по точному соответствию. Это означает, что мы должны перебирать дерево. Единственная разница между FindByAlbumTitle и FindByArtist заключается в индексе дерева, который они используют.
Мы начинаем как и прежде, перебирая индекс (строка 18). Обратите внимание, что в строке 20 мы определили обязательный префикс и используем форму префикса в нижнем регистре. Мы также ввели это в индекс как строчные буквы. Таким образом, мы можем получать регистронезависимый поиск.
Строка 21 фактически возвращает нас к началу всех записей, больших или равных префиксу, и, устанавливая RequiredPrefix, мы гарантируем, что мы не сможем перейти ни к одной записи, у которой нет этого префикса. Это интересный пример, потому что теперь мы перебираем все записи индекса с одинаковым префиксом. Но значения индекса это тоже список. Вот почему мы делаем это в строке 25. Получите все значения, которые соответствуют определенной записи с этим конкретным префиксом.
Значение этого фактического идентификатора, который мы используем, чтобы войти в дерево альбомов. И вот вам, нетривиальный поиск с Вороном.