Partager via


Commencez avec la recherche de contenu d’application

Utilisez la recherche de contenu d’application pour créer un index sémantique de votre contenu dans l’application. Cela permet aux utilisateurs de trouver des informations basées sur la signification, plutôt que sur des mots clés. L’index peut également être utilisé pour améliorer les assistants IA avec des connaissances spécifiques au domaine pour obtenir des résultats plus personnalisés et contextuels.

Plus précisément, vous allez apprendre à utiliser l’API AppContentIndexer pour :

  • Créer ou ouvrir un index du contenu dans votre application
  • Ajouter des chaînes de texte à l’index, puis exécuter une requête
  • Gérer la complexité des chaînes de texte longues
  • Indexer les données d’image, puis rechercher des images pertinentes
  • Activer les scénarios RAG (Retrieval-Augmented Generation)
  • Utiliser AppContentIndexer sur un thread d’arrière-plan
  • Fermez AppContentIndexer lorsqu’il n’est plus utilisé pour libérer des ressources

Prerequisites

Pour en savoir plus sur la configuration matérielle requise de l’API Windows AI et sur la façon de configurer votre appareil pour générer des applications avec succès à l’aide des API Windows AI, consultez Prise en main de la création d’une application avec des API Windows AI.

Condition requise pour l’identité du package

Les applications utilisant AppContentIndexer doivent avoir une identité de package, qui est disponible uniquement pour les applications empaquetées (y compris celles avec des emplacements externes). Pour activer l’indexation sémantique et la reconnaissance de texte (OCR), l’application doit également déclarer la systemaimodels fonctionnalité.

Créer ou ouvrir un index du contenu dans votre application

Pour créer un index sémantique du contenu dans votre application, vous devez d’abord établir une structure pouvant faire l’objet d’une recherche que votre application peut utiliser pour stocker et récupérer du contenu efficacement. Cet index agit comme un moteur de recherche sémantique et lexical local pour le contenu de votre application.

Pour utiliser l’API AppContentIndexer , appelez GetOrCreateIndex d’abord avec un nom d’index spécifié. Si un index portant ce nom existe déjà pour l’identité et l’utilisateur actuels de l’application, il est ouvert ; sinon, une nouvelle est créée.

public void SimpleGetOrCreateIndexSample()
{
    GetOrCreateIndexResult result = AppContentIndexer.GetOrCreateIndex("myindex");
    if (!result.Succeeded)
    {
        throw new InvalidOperationException($"Failed to open index. Status = '{result.Status}', Error = '{result.ExtendedError}'");
    }
    // If result.Succeeded is true, result.Status will either be CreatedNew or OpenedExisting
    if (result.Status == GetOrCreateIndexStatus.CreatedNew)
    {
        Console.WriteLine("Created a new index");
    }
    else if(result.Status == GetOrCreateIndexStatus.OpenedExisting)
    {
        Console.WriteLine("Opened an existing index");
    }
    using AppContentIndexer indexer = result.Indexer;
    // Use indexer...
}

Cet exemple montre la gestion des erreurs en cas d’échec de l’ouverture d’un index. Par souci de simplicité, d’autres exemples de ce document peuvent ne pas afficher la gestion des erreurs.

Ajouter des chaînes de texte à l’index, puis exécuter une requête

Cet exemple montre comment ajouter des chaînes de texte à l’index créé pour votre application, puis exécuter une requête sur cet index pour récupérer des informations pertinentes.

    // This is some text data that we want to add to the index:
    Dictionary<string, string> simpleTextData = new Dictionary<string, string>
    {
        {"item1", "Here is some information about Cats: Cats are cute and fluffy. Young cats are very playful." },
        {"item2", "Dogs are loyal and affectionate animals known for their companionship, intelligence, and diverse breeds." },
        {"item3", "Fish are aquatic creatures that breathe through gills and come in a vast variety of shapes, sizes, and colors." },
        {"item4", "Broccoli is a nutritious green vegetable rich in vitamins, fiber, and antioxidants." },
        {"item5", "Computers are powerful electronic devices that process information, perform calculations, and enable communication worldwide." },
        {"item6", "Music is a universal language that expresses emotions, tells stories, and connects people through rhythm and melody." },
    };

    public void SimpleTextIndexingSample()
    {
        AppContentIndexer indexer = GetIndexerForApp();
        // Add some text data to the index:
        foreach (var item in simpleTextData)
        {
            IndexableAppContent textContent = AppManagedIndexableAppContent.CreateFromString(item.Key, item.Value);
            indexer.AddOrUpdate(textContent);
        }
    }

    public void SimpleTextQueryingSample()
    {
        AppContentIndexer indexer = GetIndexerForApp();
        // We search the index using a semantic query:
        AppIndexTextQuery queryCursor = indexer.CreateTextQuery("Facts about kittens.");
        IReadOnlyList<TextQueryMatch> textMatches = queryCursor.GetNextMatches(5);
        // Nothing in the index exactly matches what we queried but item1 is similar to the query so we expect
        // that to be the first match.
        foreach (var match in textMatches)
        {
            Console.WriteLine(match.ContentId);
            if (match.ContentKind == QueryMatchContentKind.AppManagedText)
            {
                AppManagedTextQueryMatch textResult = (AppManagedTextQueryMatch)match;
                // Only part of the original string may match the query. So we can use TextOffset and TextLength to extract the match.
                // In this example, we might imagine that the substring "Cats are cute and fluffy" from "item1" is the top match for the query.
                string matchingData = simpleTextData[match.ContentId];
                string matchingString = matchingData.Substring(textResult.TextOffset, textResult.TextLength);
                Console.WriteLine(matchingString);
            }
        }
    }

QueryMatch inclut uniquement ContentId et TextOffset/TextLengthnon le texte correspondant lui-même. Il incombe au développeur de l’application de référencer le texte d’origine. Les résultats de la requête sont triés par pertinence, le résultat le plus important étant le plus pertinent. L’indexation se produit de manière asynchrone, de sorte que les requêtes peuvent s’exécuter sur des données partielles. Vous pouvez vérifier l’état d’indexation comme indiqué ci-dessous.

Gérer la complexité des chaînes de texte longues

L’exemple montre qu’il n’est pas nécessaire pour le développeur de l’application de diviser le contenu du texte en sections plus petites pour le traitement du modèle. AppContentIndexer gère cet aspect de la complexité.

    Dictionary<string, string> textFiles = new Dictionary<string, string>
    {
        {"file1", "File1.txt" },
        {"file2", "File2.txt" },
        {"file3", "File3.txt" },
    };
    public void TextIndexingSample2()
    {
        AppContentIndexer indexer = GetIndexerForApp();
        var folderPath = Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
        // Add some text data to the index:
        foreach (var item in textFiles)
        {
            string contentId = item.Key;
            string filename = item.Value;
            // Note that the text here can be arbitrarily large. The AppContentIndexer will take care of chunking the text
            // in a way that works effectively with the underlying model. We do not require the app author to break the text
            // down into small pieces.
            string text = File.ReadAllText(Path.Combine(folderPath, filename));
            IndexableAppContent textContent = AppManagedIndexableAppContent.CreateFromString(contentId, text);
            indexer.AddOrUpdate(textContent);
        }
    }

    public void TextIndexingSample2_RunQuery()
    {
        AppContentIndexer indexer = GetIndexerForApp();
        var folderPath = Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
        // Search the index
        AppIndexTextQuery query = indexer.CreateTextQuery("Facts about kittens.");
        IReadOnlyList<TextQueryMatch> textMatches = query.GetNextMatches(5);
        if (textMatches != null) 
        {
            foreach (var match in textMatches)
            {
                Console.WriteLine(match.ContentId);
                if (match is AppManagedTextQueryMatch textResult)
                {
                    // We load the content of the file that contains the match:
                    string matchingFilename = textFiles[match.ContentId];
                    string fileContent = File.ReadAllText(Path.Combine(folderPath, matchingFilename));
    
                    // Find the substring within the loaded text that contains the match:
                    string matchingString = fileContent.Substring(textResult.TextOffset, textResult.TextLength);
                    Console.WriteLine(matchingString);
                }
            }
        }
    }

Les données texte sont sources à partir de fichiers, mais seulement le contenu est indexé, pas les fichiers eux-mêmes. AppContentIndexer n’a aucune connaissance des fichiers d’origine et ne surveille pas les mises à jour. Si le contenu du fichier change, l’application doit mettre à jour l’index manuellement.

Indexer les données d’image, puis rechercher des images pertinentes

Cet exemple montre comment indexer les données d’image, SoftwareBitmaps puis rechercher des images pertinentes à l’aide de requêtes de texte.

    // We load the image data from a set of known files and send that image data to the indexer.
    // The image data does not need to come from files on disk, it can come from anywhere.
    Dictionary<string, string> imageFilesToIndex = new Dictionary<string, string>
        {
            {"item1", "Cat.jpg" },
            {"item2", "Dog.jpg" },
            {"item3", "Fish.jpg" },
            {"item4", "Broccoli.jpg" },
            {"item5", "Computer.jpg" },
            {"item6", "Music.jpg" },
        };
    public void SimpleImageIndexingSample()
    {
        AppContentIndexer indexer = GetIndexerForApp();

        // Add some image data to the index.
        foreach (var item in imageFilesToIndex)
        {
            var file = item.Value;
            var softwareBitmap = Helpers.GetSoftwareBitmapFromFile(file);
            IndexableAppContent imageContent = AppManagedIndexableAppContent.CreateFromBitmap(item.Key, softwareBitmap);
            indexer.AddOrUpdate(imageContent);
        }
    }
    public void SimpleImageIndexingSample_RunQuery()
    {
        AppContentIndexer indexer = GetIndexerForApp();
        // We query the index for some data to match our text query.
        AppIndexImageQuery query = indexer.CreateImageQuery("cute pictures of kittens");
        IReadOnlyList<ImageQueryMatch> imageMatches = query.GetNextMatches(5);
        // One of the images that we indexed was a photo of a cat. We expect this to be the first match to match the query.
        foreach (var match in imageMatches)
        {
            Console.WriteLine(match.ContentId);
            if (match.ContentKind == QueryMatchContentKind.AppManagedImage)
            {
                AppManagedImageQueryMatch imageResult = (AppManagedImageQueryMatch)match;
                var matchingFileName = imageFilesToIndex[match.ContentId];

                // It might be that the match is at a particular region in the image. The result includes
                // the subregion of the image that includes the match.

                Console.WriteLine($"Matching file: '{matchingFileName}' at location {imageResult.Subregion}");
            }
        }
    }

Activer les scénarios RAG (Retrieval-Augmented Generation)

RAG (Retrieval-Augmented Generation) implique d’enrichir les requêtes utilisateur aux modèles de langage avec des données pertinentes supplémentaires qui peuvent être utilisées pour générer des réponses. La requête de l’utilisateur sert d’entrée pour la recherche sémantique, qui identifie les informations pertinentes dans un index. Les données résultantes de la recherche sémantique sont ensuite incorporées dans l’invite donnée au modèle de langage afin que des réponses plus précises et plus contextuelles puissent être générées.

Cet exemple montre comment l’API AppContentIndexer peut être utilisée avec un LLM pour ajouter des données contextuelles à la requête de recherche de l’utilisateur de votre application. L’exemple est générique, aucun LLM n’est spécifié et l’exemple interroge uniquement les données locales stockées dans l’index créé (aucun appel externe à Internet). Dans cet exemple, Helpers.GetUserPrompt() et ne Helpers.GetResponseFromChatAgent() sont pas des fonctions réelles et sont simplement utilisées pour fournir un exemple.

Pour activer des scénarios RAG avec l’API AppContentIndexer , vous pouvez suivre cet exemple :

    public void SimpleRAGScenario()
    {
        AppContentIndexer indexer = GetIndexerForApp();
        // These are some text files that had previously been added to the index.
        // The key is the contentId of the item.
        Dictionary<string, string> data = new Dictionary<string, string>
        {
            {"file1", "File1.txt" },
            {"file2", "File2.txt" },
            {"file3", "File3.txt" },
        };
        string userPrompt = Helpers.GetUserPrompt();
        // We execute a query against the index using the user's prompt string as the query text.
        AppIndexTextQuery query = indexer.CreateTextQuery(userPrompt);
        IReadOnlyList<TextQueryMatch> textMatches = query.GetNextMatches(5);
        StringBuilder promptStringBuilder = new StringBuilder();
        promptStringBuilder.AppendLine("Please refer to the following pieces of information when responding to the user's prompt:");
        // For each of the matches found, we include the relevant snippets of the text files in the augmented query that we send to the language model
        foreach (var match in textMatches)
        {
            if (match is AppManagedTextQueryMatch textResult)
            {
                // We load the content of the file that contains the match:
                string matchingFilename = data[match.ContentId];
                string fileContent = File.ReadAllText(matchingFilename);
                // Find the substring within the loaded text that contains the match:
                string matchingString = fileContent.Substring(textResult.TextOffset, textResult.TextLength);
                promptStringBuilder.AppendLine(matchingString);
                promptStringBuilder.AppendLine();
            }
        }
        promptStringBuilder.AppendLine("Please provide a response to the following user prompt:");
        promptStringBuilder.AppendLine(userPrompt);
        var response = Helpers.GetResponseFromChatAgent(promptStringBuilder.ToString());
        Console.WriteLine(response);
    }

Utiliser AppContentIndexer sur un thread d’arrière-plan

Une instance AppContentIndexer n’est pas associée à un thread particulier ; il s’agit d’un objet agile qui peut fonctionner sur des threads. Certaines méthodes d’AppContentIndexer et de ses types associés peuvent nécessiter un temps de traitement considérable. Par conséquent, il est conseillé d’éviter d’appeler des API AppContentIndexer directement à partir du thread d’interface utilisateur de l’application et d’utiliser plutôt un thread d’arrière-plan.

Fermez AppContentIndexer lorsqu’il n’est plus utilisé pour libérer des ressources

AppContentIndexer implémente l’interface IClosable pour déterminer sa durée de vie. L’application doit fermer l’indexeur lorsqu’elle n’est plus utilisée. Cela permet à AppContentIndexer de libérer ses ressources sous-jacentes.

    public void IndexerDisposeSample()
    {
        var indexer = AppContentIndexer.GetOrCreateIndex("myindex").Indexer;
        // use indexer
        indexer.Dispose();
        // after this point, it would be an error to try to use indexer since it is now Closed.
    }

Dans le code C#, l’interface IClosable est projetée en tant que IDisposable. Le code C# peut utiliser le using modèle pour les instances AppContentIndexer .

    public void IndexerUsingSample()
    {
        using var indexer = AppContentIndexer.GetOrCreateIndex("myindex").Indexer;
        // use indexer
        //indexer.Dispose() is automatically called
    }

Si vous ouvrez le même index plusieurs fois dans votre application, vous devez appeler Close sur chaque instance.

L’ouverture et la fermeture d’un index constituent une opération coûteuse. Vous devez donc réduire ces opérations dans votre application. Par exemple, une application peut stocker une seule instance de l’AppContentIndexer pour l’application et utiliser cette instance tout au long de la durée de vie de l’application au lieu d’ouvrir et de fermer constamment l’index pour chaque action qui doit être effectuée.