Compartir a través de


Introducción a la búsqueda de contenido de la aplicación

Use búsqueda de contenido de la aplicación para crear un índice semántico del contenido en la aplicación. Esto permite a los usuarios encontrar información basada en el significado, en lugar de simplemente palabras clave. El índice también se puede usar para mejorar los asistentes de inteligencia artificial con conocimientos específicos del dominio para obtener resultados más personalizados y contextuales.

En concreto, aprenderá a usar la API AppContentIndexer para:

  • Crear o abrir un índice del contenido de la aplicación
  • Agregar cadenas de texto al índice y, a continuación, ejecutar una consulta
  • Administración de la complejidad de cadenas de texto largas
  • Indexar datos de imagen y, a continuación, buscar imágenes relevantes
  • Habilitar escenarios RAG (Retrieval-Augmented Generation)
  • Uso del AppContentIndexer en un hilo en segundo plano.
  • Cierre AppContentIndexer cuando ya no esté en uso para liberar recursos

Prerrequisitos

Para obtener información sobre los requisitos de hardware de la API de IA de Windows y cómo configurar el dispositivo para compilar correctamente aplicaciones mediante las API de ia de Windows, consulte Introducción a la creación de una aplicación con las API de IA de Windows.

Requisito de identidad del paquete

Las aplicaciones que usan AppContentIndexer deben tener la identidad del paquete, que solo está disponible para las aplicaciones empaquetadas (incluidas las que tienen ubicaciones externas). Para habilitar la indexación semántica y el reconocimiento de texto (OCR), la aplicación también debe declarar la systemaimodels funcionalidad.

Crear o abrir un índice del contenido de la aplicación

Para crear un índice semántico del contenido en la aplicación, primero debes establecer una estructura que se pueda buscar que la aplicación pueda usar para almacenar y recuperar contenido de forma eficaz. Este índice actúa como un motor de búsqueda semántico y léxico local para el contenido de la aplicación.

Para usar la API AppContentIndexer , primero llame a GetOrCreateIndex con un nombre de índice especificado. Si ya existe un índice con ese nombre para la identidad y el usuario de la aplicación actual, se abre; de lo contrario, se crea uno nuevo.

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...
}

En este ejemplo se muestra el manejo del caso de fallo al abrir un índice. Por motivos de simplicidad, es posible que otros ejemplos de este documento no muestren el control de errores.

Agregar cadenas de texto al índice y, a continuación, ejecutar una consulta

En este ejemplo se muestra cómo agregar algunas cadenas de texto al índice creado para la aplicación y, a continuación, ejecutar una consulta en ese índice para recuperar información relevante.

    // 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 incluye solo ContentId y TextOffset/TextLength, no el propio texto coincidente. Es responsabilidad suya como desarrollador de aplicaciones hacer referencia al texto original. Los resultados de la consulta se ordenan por relevancia, con el resultado más relevante. La indexación se produce de forma asincrónica, por lo que las consultas se pueden ejecutar en datos parciales. Puede comprobar el estado de indexación como se describe a continuación.

Administración de la complejidad de cadenas de texto largas

En el ejemplo se muestra que no es necesario que el desarrollador de la aplicación divida el contenido de texto en secciones más pequeñas para el procesamiento de modelos. AppContentIndexer administra este aspecto de complejidad.

    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);
                }
            }
        }
    }

Los datos de texto se originan a partir de archivos, pero solo se indexa el contenido, no los propios archivos. AppContentIndexer no tiene conocimiento de los archivos originales y no supervisa las actualizaciones. Si cambia el contenido del archivo, la aplicación debe actualizar el índice manualmente.

Indexar datos de imagen y, a continuación, buscar imágenes relevantes

En este ejemplo se muestra cómo indexar los datos de imagen como SoftwareBitmaps y, a continuación, buscar imágenes relevantes mediante consultas de texto.

    // 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}");
            }
        }
    }

Habilitar escenarios RAG (Retrieval-Augmented Generation)

RAG (Retrieval-Augmented Generation) implica aumentar las consultas de usuario a los modelos de lenguaje con datos pertinentes adicionales que se pueden usar para generar respuestas. La consulta del usuario actúa como entrada para la búsqueda semántica, que identifica información pertinente en un índice. A continuación, los datos resultantes de la búsqueda semántica se incorporan en el prompt proporcionado al modelo de lenguaje para que se puedan generar respuestas más precisas y conscientes del contexto.

En este ejemplo se muestra cómo se puede usar la API appContentIndexer con un LLM para agregar datos contextuales a la consulta de búsqueda del usuario de la aplicación. El ejemplo es genérico, no se especifica LLM y el ejemplo solo consulta los datos locales almacenados en el índice creado (sin llamadas externas a Internet). En este ejemplo, Helpers.GetUserPrompt() y Helpers.GetResponseFromChatAgent() no son funciones reales y solo se usan para proporcionar un ejemplo.

Para habilitar escenarios RAG con la API AppContentIndexer , puede seguir este ejemplo:

    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);
    }

Uso del AppContentIndexer en un hilo en segundo plano.

Una instancia de AppContentIndexer no está asociada a un subproceso determinado; es un objeto ágil que puede funcionar entre subprocesos. Algunos métodos de AppContentIndexer y sus tipos relacionados pueden requerir un tiempo de procesamiento considerable. Por lo tanto, es aconsejable evitar invocar api de AppContentIndexer directamente desde el subproceso de interfaz de usuario de la aplicación y, en su lugar, usar un subproceso en segundo plano.

Cierre AppContentIndexer cuando ya no esté en uso para liberar recursos

AppContentIndexer implementa la IClosable interfaz para determinar su duración. La aplicación debe cerrar el indexador cuando ya no esté en uso. Esto permite a AppContentIndexer liberar sus recursos subyacentes.

    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.
    }

En el código de C#, la IClosable interfaz se proyecta como IDisposable. El código de C# puede usar el using patrón para las instancias de AppContentIndexer .

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

Si abre el mismo índice varias veces en su aplicación, debe llamar a Close en cada instancia.

Abrir y cerrar un índice es una operación costosa, por lo que debe minimizar estas operaciones en la aplicación. Por ejemplo, una aplicación podría almacenar una sola instancia de AppContentIndexer para la aplicación y usar esa instancia durante toda la vigencia de la aplicación en lugar de abrir y cerrar constantemente el índice para cada acción que se debe realizar.