Search documents

Before you start

To ingest sample documents into the Document AI Warehouse, see the Quickstart Guide.

When defining your document schemas and creating your documents, it's important to consider what properties you want to define and how they're going to be used with search, if at all.

Mark a property filterable if you want to use that property to include or exclude a portion of documents for a search. For example, you might make a property that represents a "Vendor" filterable because your users want to search for invoices from a specific vendor.

If you want to construct a histogram (see the example later in this topic) on a property, then the property needs to be filterable.

Mark a property searchable if it has data that your users will want to query during a keyword search.

Full text search is the process of retrieving all documents that match the search keywords in their searchable text. The user provides a list of keywords (words separated by a blank space), presumably typed into a search field in the UI. In Document AI Warehouse, the keywords are processed and converted into a proper query. Such processing strips stopwords ("the," "in," and " an, ") and stems the remaining words. Stemming reduces the word to a common version of the wording, so that word variation matches. For example: "work," "working," "worked."

What data gets searched?

  • The document's plain_text.
  • If you are importing a Document AI object, use the embedded cloud_ai_document.text.
  • The document's display_name.
  • All searchable properties.

The query partially supports Google AIP style syntax. Specifically, the query supports literals, logical operators, negation operators, comparison operators, and functions.

  • Literals: A bare literal value (examples: "42", "Hugo") is a value to be matched against. It searches over the full text of the document and the searchable properties.
  • Logical operators: "AND", "and", "OR", and "or" are binary logical operators (example: "engineer OR developer").
  • Negation operators: "NOT" and "!" are negation operators (example: "NOT software").
  • Comparison operators: support the binary comparison operators =, !=, <, >, <= and >= for string, numeric, enum, boolean. Also support like operator ~~ for string. It provides semantic search functionality by parsing, stemming and doing synonyms expansion against the input query.

    To specify a property in the query, the left hand side expression in the comparison must be the property ID including the parent. The right hand side must be literals. For example: \"projects/123/locations/us\".property_a < 1 matches results whose property_a is less than 1 in project 123 and us location. The literals and comparison expression can be connected in a single query (example: software engineer \"projects/123/locations/us\".salary > 100).

  • Functions: supported functions are LOWER([property_name]) to perform a case insensitive match and EMPTY([property_name]) to filter on the existence of a key.

  • Support nested expressions connected using parentheses and logical operators. The default logical operators is AND if there is no operators between expressions.

The query can be used with other filters e.g. time_filters and folder_name_filter. They are connected with AND operator under the hood.

Search queries can be filtered by additional parameters such as by property, time, schema, folder, and creator.

Call to a search request

To call the search service, you must use a search request, which is defined as follows:

{
  "requestMetadata": {
    object (RequestMetadata)
  },
  "documentQuery": {
    object (DocumentQuery)
  },
  "offset": integer,
  "pageSize": integer,
  "pageToken": string,
  "orderBy": string,
  "histogramQueries": [
    {
      object (HistogramQuery)
    }
  ],
  "requireTotalSize": boolean,
  "totalResultSize": enum (TotalResultSize),
  "qaSizeLimit": integer
}

The parent field must be filled in with the format:

/projects/PROJECT_ID/locations/LOCATION

Response to a search request

The search response is defined as follows:

{
  "matchingDocuments": [
    {
      object (MatchingDocument)
    }
  ],
  "nextPageToken": string,
  "totalSize": integer,
  "metadata": {
    object (ResponseMetadata)
  },
  "histogramQueryResults": [
    {
      object (HistogramQueryResult)
    }
  ]
}

Document Query

The document_query field is defined as follows:

{
  "query": string,
  "isNlQuery": boolean,
  "customPropertyFilter": string,
  "timeFilters": [
    {
      object (TimeFilter)
    }
  ],
  "documentSchemaNames": [
    string
  ],
  "propertyFilter": [
    {
      object (PropertyFilter)
    }
  ],
  "fileTypeFilter": {
    object (FileTypeFilter)
  },
  "folderNameFilter": string,
  "queryContext": [
    string
  ],
  "documentCreatorFilter": [
    string
  ],
  "customWeightsMetadata": {
    object (CustomWeightsMetadata)
  }
}

The query field is for the requesting user's search query words. Typically, these come from the search field in the UI.

Filters

Document AI Warehouse offers a variety of filters.

Document time filter

The create and update time filter is exactly what you would expect: it finds documents matching the keywords within a specified time period.

A TimeFilter object is used to specify the time range and it is defined as follows:

{
  "timeRange": {
    object (Interval)
  },
  "timeField": enum (TimeField)
}

The time_field field is where you specify if the time range specified in the time_range is for the document's creation time or the document's last update time.

The time_range field specifies the time range as an Interval. An Interval is defined as:

{
  "startTime": string,
  "endTime": string
}

Creator filter

To search for documents that were created by specific user or users then use the creator filter. For example:

  {
    document_query {
      query: "videogames director",
      documentCreatorFilter: [
        "diane@some_company.com",
        "frank@some_company.com",
      ],
    },
  }

Property filter

The property filter lets you specify filters on any of the properties that you have specified in a schema, as long as that property has been configured to be filterable.

For example, using property filters in the legal industry might filter on a property called COURT to search only documents from a particular court.

Property filters use a PropertyFilter object. You can have more than one property filter. When you use multiple property filters, they are combined using the OR operator. A property filter is defined as follows:

  {
    "documentSchemaName": string,
    "condition": string
  }

Properties are defined in schemas. Thus, the documentSchemaName field is where you specify the schema for the property that you use for filtering. In the condition field, you specify the desired logic. For examples of using the documentSchemaName and condition fields, see the preceding examples on this page.

Matching document

A matching document contains a Document and a snippet (discussed later). The returned document in MatchingDocument is not a fully filled-in document. It contains minimal data for displaying a search results list to the requesting user. If the full document is desired (for example, if the user clicked on a search result), then the full document should be retrieved via the GetDocument API.

The following Document fields are filled in: Project number, Document id, Document schema id, Create time, Update time, Display name, Raw document file type, Reference id, and Filterable properties.

A matching document would look like this:

{
  "document": {
    object (Document)
  },
  "searchTextSnippet": string,
  "qaResult": {
    object (QAResult)
  }
}

Ranking/sort

The search request lets you specify how you want the results sorted. To sort, use the order_by field in the search request. The possible values for this field include:

  • relevance desc - relevance descending, that is, the best matches are on top.
  • upload_date desc - the date the document was created in descending order (newest on top).
  • upload_date - the date the document was created in ascending order (oldest on top).
  • update_date desc - the date the document was last updated in descending order (newest on top).
  • Update_date - the date the document was last updated in ascending order (oldest on top).

If you don't specify a sort, but you supply search keywords, then the sort is by relevance descending (the best matches on top). If neither the sort nor keywords are provided, then the default sort is by update time descending (the latest documents on top).

Pagination

Pagination is useful for displaying a page worth of data to the end user. Here you can specify the size of the page and get a total count of the result size to display back to the user (for example, "Showing 50 documents of 300").

Set the page_size field to the desired number of results that you want to receive with the search request. This might correspond to the requirements of the UI search result display size.

There are two mechanisms: offset and page token.

An offset is the index into the list of returnable documents that you want returned. For example, an offset of 5 means you want the sixth document onward. Presumably you would increment the offset by the page size for the next page of results.

Alternatively, you can use a page token and not have to worry about calculating the next offset. After making your first search request, you get a search response that contains the next_page_token field. If this field is empty, then there are no more results. If the field is not empty, use this token in your next search request by setting the page_token field.

Some UIs display the count of documents found by the search. For example, you are viewing 10 documents of 120. To get a document count returned, set the request's require_total_size boolean field to True. Tip: require_total_size=True carries a performance penalty. Set this on the first page query, then set this to false on all subsequent requests, keeping the total count in a local variable.

Code Samples

Python

For more information, see the Document AI Warehouse Python API reference documentation.

To authenticate to Document AI Warehouse, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.


from google.cloud import contentwarehouse

# TODO(developer): Uncomment these variables before running the sample.
# project_number = 'YOUR_PROJECT_NUMBER'
# location = 'YOUR_PROJECT_LOCATION' # Format is 'us' or 'eu'
# document_query_text = 'YOUR_DOCUMENT_QUERY'
# user_id = 'user:YOUR_SERVICE_ACCOUNT_ID' # Format is "user:xxxx@example.com"


def search_documents_sample(
    project_number: str, location: str, document_query_text: str, user_id: str
) -> None:
    # Create a client
    client = contentwarehouse.DocumentServiceClient()

    # The full resource name of the location, e.g.:
    # projects/{project_number}/locations/{location}
    parent = client.common_location_path(project=project_number, location=location)

    # File Type Filter
    # Options: DOCUMENT, FOLDER
    file_type_filter = contentwarehouse.FileTypeFilter(
        file_type=contentwarehouse.FileTypeFilter.FileType.DOCUMENT
    )

    # Document Text Query
    document_query = contentwarehouse.DocumentQuery(
        query=document_query_text,
        file_type_filter=file_type_filter,
    )

    # Histogram Query
    histogram_query = contentwarehouse.HistogramQuery(
        histogram_query='count("DocumentSchemaId")'
    )

    request_metadata = contentwarehouse.RequestMetadata(
        user_info=contentwarehouse.UserInfo(id=user_id)
    )

    # Define request
    request = contentwarehouse.SearchDocumentsRequest(
        parent=parent,
        request_metadata=request_metadata,
        document_query=document_query,
        histogram_queries=[histogram_query],
    )

    # Make the request
    response = client.search_documents(request=request)

    # Print search results
    for matching_document in response.matching_documents:
        document = matching_document.document
        # Display name - schema display name.
        # Name.
        # Create date.
        # Snippet - keywords are highlighted with <b> & </b>.
        print(
            f"{document.display_name} - {document.document_schema_name}\n"
            f"{document.name}\n"
            f"{document.create_time}\n"
            f"{matching_document.search_text_snippet}\n"
        )

    # Print histogram
    for histogram_query_result in response.histogram_query_results:
        print(
            f"Histogram Query: {histogram_query_result.histogram_query}\n"
            f"| {'Schema':<70} | {'Count':<15} |"
        )
        for key, value in histogram_query_result.histogram.items():
            print(f"| {key:<70} | {value:<15} |")

Java

For more information, see the Document AI Warehouse Java API reference documentation.

To authenticate to Document AI Warehouse, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.

import com.google.cloud.contentwarehouse.v1.DocumentQuery;
import com.google.cloud.contentwarehouse.v1.DocumentServiceClient;
import com.google.cloud.contentwarehouse.v1.DocumentServiceClient.SearchDocumentsPagedResponse;
import com.google.cloud.contentwarehouse.v1.DocumentServiceSettings;
import com.google.cloud.contentwarehouse.v1.FileTypeFilter;
import com.google.cloud.contentwarehouse.v1.FileTypeFilter.FileType;
import com.google.cloud.contentwarehouse.v1.LocationName;
import com.google.cloud.contentwarehouse.v1.RequestMetadata;
import com.google.cloud.contentwarehouse.v1.SearchDocumentsRequest;
import com.google.cloud.contentwarehouse.v1.SearchDocumentsResponse.MatchingDocument;
import com.google.cloud.contentwarehouse.v1.UserInfo;
import com.google.cloud.resourcemanager.v3.Project;
import com.google.cloud.resourcemanager.v3.ProjectName;
import com.google.cloud.resourcemanager.v3.ProjectsClient;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

public class SearchDocuments {
  public static void main(String[] args) throws IOException, 
        InterruptedException, ExecutionException, TimeoutException { 
    // TODO(developer): Replace these variables before running the sample.
    String projectId = "your-project-id";
    String location = "your-region"; // Format is "us" or "eu".
    String documentQuery = "your-document-query";
    String userId = "your-user-id"; // Format is user:<user-id>

    searchDocuments(projectId, location, documentQuery, userId);
  }

  // Searches all documents for a given Document Query
  public static void searchDocuments(String projectId, String location,
        String documentQuery, String userId) throws IOException, InterruptedException,
          ExecutionException, TimeoutException { 
    String projectNumber = getProjectNumber(projectId);

    String endpoint = "contentwarehouse.googleapis.com:443";
    if (!"us".equals(location)) {
      endpoint = String.format("%s-%s", location, endpoint);
    }

    DocumentServiceSettings documentServiceSettings = 
             DocumentServiceSettings.newBuilder().setEndpoint(endpoint)
             .build(); 

    /*
     * Create the Document Service Client 
     * Initialize client that will be used to send requests. 
     * This client only needs to be created once, and can be reused for multiple requests. 
     */
    try (DocumentServiceClient documentServiceClient = 
            DocumentServiceClient.create(documentServiceSettings)) {  

      /*
       * The full resource name of the location, e.g.:
       * projects/{project_number}/locations/{location} 
       */
      String parent = LocationName.format(projectNumber, location);

      // Define RequestMetadata object for context of the user making the API call
      RequestMetadata requestMetadata = RequestMetadata.newBuilder()
          .setUserInfo(
          UserInfo.newBuilder()
            .setId(userId)
            .build())
            .build();

      // Set file type for filter to 'DOCUMENT'
      FileType documentFileType = FileType.DOCUMENT;

      // Create a file type filter for documents 
      FileTypeFilter fileTypeFilter = FileTypeFilter.newBuilder()
          .setFileType(documentFileType)
          .build();

      // Create document query to search all documents for text given at input
      DocumentQuery query = DocumentQuery.newBuilder()
          .setQuery(documentQuery)
          .setFileTypeFilter(fileTypeFilter)
          .build();

      /*
       * Create the request to search all documents for specified query. 
       * Please note the offset in this request is to only return the specified number of results 
       * to avoid hitting the API quota. 
       */
      SearchDocumentsRequest searchDocumentsRequest = SearchDocumentsRequest.newBuilder()
          .setParent(parent)
          .setRequestMetadata(requestMetadata)
          .setOffset(5)
          .setDocumentQuery(query)
          .build();

      // Make the call to search documents with document service client and store the response
      SearchDocumentsPagedResponse searchDocumentsPagedResponse = 
          documentServiceClient.searchDocuments(searchDocumentsRequest);

      // Iterate through response and print search results for documents matching the search query
      for (MatchingDocument matchingDocument :
          searchDocumentsPagedResponse.iterateAll()) {
        System.out.println(
            "Display Name: " + matchingDocument.getDocument().getDisplayName()
            + "Document Name: " + matchingDocument.getDocument().getName()
            + "Document Creation Time: " + matchingDocument.getDocument().getCreateTime().toString()
            + "Search Text Snippet: " + matchingDocument.getSearchTextSnippet());
      }
    }
  }

  private static String getProjectNumber(String projectId) throws IOException { 
    /*
     * Initialize client that will be used to send requests. 
     * This client only needs to be created once, and can be reused for multiple requests.
     */
    try (ProjectsClient projectsClient = ProjectsClient.create()) { 
      ProjectName projectName = ProjectName.of(projectId); 
      Project project = projectsClient.getProject(projectName);
      String projectNumber = project.getName(); // Format returned is projects/xxxxxx
      return projectNumber.substring(projectNumber.lastIndexOf("/") + 1);
    } 
  }
}

Next steps