Jedním z omezení Azure SQL databází je nemožnost používat SQL Server Fulltext. Je nutné použít některou z alternativ, jako například Lucene.Net, což je open source knihovna, která fulltextové hledání implementuje. Data si tato knihovna standardně ukládá do filesystému, ale díky rozšíření Lucene.Net.Store.Azure umožňuje ukládat index i do blob storage, díky čemuž je snadno použitelná v cloudu i bez nutnosti vytváření virtual machine.
Nejprve je třeba přidat knihovny Lucene do projektu – stačí pomocí NuGetu nainstalovat balíček Lucene.Net.Store.Azure, NuGet si už dohledá všechny potřebné závislosti včetně Lucene samotného a přidá je do projektu.
indexování dat
Data, ve kterých chcete hledat, se v terminologii Lucene nazývají dokumenty. Vzhledem k tomu, že data držíme primárně v SQL databázi a z Lucene chceme používat jen fulltext, stačí nám indexovat pouze primární klíč každého dokumentu, který se používá v SQL Serveru, a dále pak všechny textové sloupce, které chceme prohledávat.
Pro přidávání dat do indexu slouží v Lucene třída IndexWriter. Přidání dokumentu do indexu může vypadat nějak takto:
public void AddToIndex()
{
var document = new Document();
document.Add(new Field("Id", "id z databáze", Field.Store.YES, Field.Index.NO, Field.TermVector.NO));
document.Add(new Field("Title", "tohle je název", Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.NO));
document.Add(new Field("Text", "tohle je text", Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.NO));
using (var indexWriter = CreateIndexWriter())
{
indexWriter.AddDocument(document);
indexWriter.Optimize();
}
}
private IndexWriter CreateIndexWriter()
{
var account = CloudStorageAccount.Parse("connection string do blob storage");
var directory = new AzureDirectory(account, "nazev-storage-kontejneru");
try
{
// open existing index
return new IndexWriter(directory, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), false, Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED);
}
catch (Exception ex)
{
// create a new index
return new IndexWriter(directory, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), true, Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED);
}
}
Každý dokument má vlastnost Id, kam uložíme primární klíč z databáze, a dále všechny textové sloupce, které chceme prohledávat – všimněme si, že mají Field.Index.ANALYZED, což znamená, že se fulltextově prohledávají – ve zkratce Lucene hodnotu projde a zaindexuje každé slovo.
Takto musíme zaindexovat všechny dokumenty, ve kterých chceme hledat.
Dotazování
Možnosti dotazování v Lucene jsou velmi široké, my si ukážeme jen to nejzákladnější.
/// <summary>
/// Searches the specified text.
/// </summary>
public IList<int> Search(string text, int skip, int take, out int totalRows)
{
using (var indexSearcher = CreateIndexSearcher())
{
var multiParser = new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, new[] { "Title", "Text" }, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30));
var results = indexSearcher.Search(multiParser.Parse(text), skip + take);
totalRows = results.TotalHits;
return results.ScoreDocs
.Skip(skip)
.Select(d => indexSearcher.Doc(d.Doc).GetField("Id").StringValue)
.Select(int.Parse)
.ToList();
}
}
private IndexSearcher CreateIndexSearcher()
{
var account = CloudStorageAccount.Parse("connection string do blob storage");
var directory = new AzureDirectory(account, "nazev-storage-kontejneru");
return new IndexSearcher(directory);
}
Nejprve se vytvoří instance třídy MultiFieldQueryParser, které musíme prozradit názvy sloupců, nad nimiž chceme hledat. Na ní pak zavoláme multiParser.Parse a předáme tam hledané slovo nebo výraz (např. cat AND mouse). Tento dotaz předáme do funkce indexSearcher.Search, která provede samotné hledání. Druhý parametr značí, kolik nejlepších výsledků má vrátit.
Pokud bychom chtěli stránkovat, tedy např. přeskočit prvních 40 záznamů a vzít následujících 20 (parametry skip a take), tak vězme, že Lucene to nijak snadno neumí (nebo jsem to nikde v dokumentaci nenašel) a musíme tedy vyzobnout všechny záznamy a těch prvních 40 zahodit. Nutno podotknout, že funkce Search vrací jen pole interních ID v Lucene a každý dokument, který nás zajímá, je nutné stejně dotáhnout přes funkci indexSearcher.Doc, které se toto ID předá, takže i kdybychom si řekli o 1000 záznamů, nebude se přenášet zase tolik dat, na výkon by to nemělo mít takový vliv.
Zbytek té funkce transformuje výsledky a vytáhne z nich jen ID dokumentů, které vyhovovaly dotazu. Víc toho potřeba není, ID vezmeme, převedeme na integer a vrátíme.
Pokud chceme výsledky zkombinovat s nějaký SQL dotazem, pak využijeme toho, že Entity Framework chápe funkci Contains na kolekcích – uděláme tedy něco takového:
int totalRows;
List<int> fulltextIds = TemplateItemIndex.Search(ErrorListFilters.Text, 0, 20, out totalRows);
return dbContext.Documents.Where(t => fulltextIds.Contains(t.Id));
Entity Framework to pak převede na něco ve stylu SELECT * FROM Documents WHERE Id IN (1,3,5,18,253…), takže data získáme na jeden dotaz.
Doporučuji do storage kontejneru, kde má Lucene svá data, nedávat nic jiného, vypadá to tam nějak takhle.