חיפוש מסמכים

לפני שמתחילים

כדי להטמיע מסמכים לדוגמה ב-Document AI Warehouse, אפשר לעיין במדריך למתחילים.

כשמגדירים את סכימות המסמכים ויוצרים את המסמכים, חשוב לחשוב אילו מאפיינים רוצים להגדיר ואיך הם ישמשו לחיפוש, אם בכלל.

כדאי לסמן מאפיין כניתן לסינון אם רוצים להשתמש בו כדי לכלול או להחריג חלק מהמסמכים בחיפוש. לדוגמה, יכול להיות שתרצו ליצור מאפיין שנקרא 'ספק' שניתן לסינון, כי המשתמשים רוצים לחפש חשבוניות מספק ספציפי.

אם רוצים ליצור היסטוגרמה (כמו בדוגמה שבהמשך הנושא הזה) בנכס, צריך לוודא שאפשר לסנן את הנכס.

כדאי לסמן נכס כנכס שאפשר לחפש בו אם יש בו נתונים שהמשתמשים ירצו לשאול עליהם שאילתות במהלך חיפוש מילות מפתח.

חיפוש טקסט מלא הוא תהליך של אחזור כל המסמכים שתואמים למילות המפתח לחיפוש בטקסט שניתן לחיפוש. המשתמש מספק רשימה של מילות מפתח (מילים שמופרדות ברווח), כנראה שהן הוקלדו בשדה חיפוש בממשק המשתמש. ב-Document AI Warehouse, מילות המפתח מעובדות ומומרות לשאילתה מתאימה. במהלך העיבוד הזה, המערכת מסירה מילים חסרות משמעות (כמו 'the',‏ 'in' ו-'an') ומבצעת גזירה של המילים שנותרו. הגזירה מצמצמת את המילה לגרסה נפוצה של הניסוח, כך שווריאציה של המילה תתאים. לדוגמה: work,‏ working,‏ worked.

אילו נתונים נכללים בחיפוש?

  • plain_text של המסמך.
  • אם מייבאים אובייקט של Document AI, צריך להשתמש ב-cloud_ai_document.text המוטמע.
  • השם המוצג של המסמך.
  • כל המאפיינים שאפשר לחפש.

השאילתה תומכת באופן חלקי בתחביר בסגנון Google AIP. באופן ספציפי, השאילתה תומכת בערכים מילוליים, באופרטורים לוגיים, באופרטורים של שלילה, באופרטורים של השוואה ובפונקציות.

  • ערכים מילוליים: ערך מילולי גולמי (דוגמאות: '42',‏ 'Hugo') הוא ערך שצריך להיות זהה לערך שמוגדר. החיפוש מתבצע בטקסט המלא של המסמך ובמאפיינים שניתן לחפש בהם.
  • אופרטורים לוגיים: האופרטורים AND,‏ and,‏ OR ו-or הם אופרטורים לוגיים בינאריים (דוגמה: engineer OR developer).
  • אופרטורים של שלילה: האופרטורים NOT ו-! הם אופרטורים של שלילה (לדוגמה: NOT software).
  • אופרטורים להשוואה: תמיכה באופרטורים להשוואה בינארית =, !=, <, >, <= ו->= למחרוזות, למספרים, לספירות ולערכים בוליאניים. תמיכה גם באופרטור ~~ כמו מחרוזת. הוא מספק פונקציונליות של חיפוש סמנטי על ידי ניתוח, גזירה והרחבה של מילים נרדפות ביחס לשאילתת הקלט.

    כדי לציין מאפיין בשאילתה, הביטוי בצד ימין של ההשוואה צריך להיות מזהה המאפיין, כולל המאפיין הראשי. הצד השמאלי חייב להיות ליטרלים. לדוגמה: \"projects/123/locations/us\".property_a < 1 תואם לתוצאות שבהן הערך של property_a קטן מ-1 בפרויקט 123 ובמיקום us. אפשר לחבר את הערכים המילוליים ואת ביטוי ההשוואה בשאילתה אחת (לדוגמה: software engineer \"projects/123/locations/us\".salary > 100).

  • פונקציות: הפונקציות הנתמכות הן LOWER([property_name]) לביצוע התאמה לא תלוית-רישיות ו-EMPTY([property_name]) לסינון לפי קיום מפתח.

  • תמיכה בביטויים מקוננים שמחוברים באמצעות סוגריים ואופרטורים לוגיים. אופרטור ברירת המחדל הוא AND אם אין אופרטורים בין הביטויים.

אפשר להשתמש בשאילתה עם מסננים אחרים, למשל time_filters ו-folder_name_filter. הם מחוברים באמצעות האופרטור AND.

אפשר לסנן את שאילתות החיפוש לפי פרמטרים נוספים כמו property,‏ time,‏ schema,‏ folder ו-creator.

קריאה לבקשת חיפוש

כדי להתקשר לשירות החיפוש, צריך להשתמש בבקשת חיפוש, שמוגדרת באופן הבא:

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

בשדה parent צריך להזין את הנתונים בפורמט הבא:

/projects/PROJECT_ID/locations/LOCATION

תשובה לבקשת חיפוש

התגובה לחיפוש מוגדרת כך:

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

שאילתת מסמך

השדה document_query מוגדר כך:

{
  "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)
  }
}

השדה query מיועד למילים בשאילתת החיפוש של המשתמש ששולח את הבקשה. בדרך כלל, הם מגיעים משדה החיפוש בממשק המשתמש.

מסננים

ב-Document AI Warehouse יש מגוון מסננים.

מסנן זמן של מסמך

המסנן של זמן היצירה והעדכון הוא בדיוק מה שאתם מצפים: הוא מוצא מסמכים שתואמים למילות המפתח בפרק זמן מוגדר.

אובייקט TimeFilter משמש לציון טווח הזמן, והוא מוגדר כך:

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

בשדה time_field מציינים אם טווח הזמן שצוין בשדה time_range מתייחס לזמן היצירה של המסמך או לזמן העדכון האחרון של המסמך.

השדה time_range מציין את טווח הזמן כ-Interval. Interval מוגדר כך:

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

מסנן יוצרים

כדי לחפש מסמכים שנוצרו על ידי משתמש או משתמשים ספציפיים, משתמשים במסנן היוצר. לדוגמה:

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

סינון לפי מאפיינים

מסנן הנכסים מאפשר לכם לציין מסננים בכל אחד מהנכסים שציינתם בסכימה, בתנאי שהנכס הזה הוגדר כנכס שאפשר לסנן.

לדוגמה, אם אתם עובדים בתחום המשפטי, תוכלו להשתמש במסנני מאפיינים כדי לסנן לפי מאפיין שנקרא COURT ולחפש רק מסמכים מבית משפט מסוים.

מסנני נכסים משתמשים באובייקט PropertyFilter. אפשר להוסיף יותר ממסנן נכס אחד. כשמשתמשים בכמה מסנני מאפיינים, הם משולבים באמצעות האופרטור OR. סינון לפי מאפיינים מוגדר כך:

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

המאפיינים מוגדרים בסכימות. לכן, בשדה documentSchemaName מציינים את הסכימה של הנכס שמשמש לסינון. בשדה condition מציינים את הלוגיקה הרצויה. דוגמאות לשימוש בשדות documentSchemaName ו-condition מופיעות בדוגמאות הקודמות בדף הזה.

מסמך תואם

מסמך תואם מכיל Document וקטע קוד (שנרחיב עליו בהמשך). המסמך שמוחזר ב-MatchingDocument לא מלא. הוא מכיל כמות מינימלית של נתונים שנדרשים כדי להציג למשתמש ששלח את הבקשה רשימה של תוצאות חיפוש. אם רוצים את המסמך המלא (למשל, אם המשתמש לחץ על תוצאת חיפוש), צריך לאחזר את המסמך המלא באמצעות GetDocument API.

השדות הבאים של Document ימולאו: Project number,‏ Document id,‏ Document schema id,‏ Create time,‏ Update time,‏ Display name,‏ Raw document file type,‏ Reference id ו-Filterable properties.

מסמך תואם ייראה כך:

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

דירוג/מיון

בבקשת החיפוש אפשר לציין איך רוצים למיין את התוצאות. כדי למיין, משתמשים בשדה order_by בבקשת החיפוש. הערכים האפשריים בשדה הזה כוללים:

  • relevance desc – רלוונטיות בסדר יורד, כלומר ההתאמות הכי טובות מופיעות למעלה.
  • upload_date desc – התאריך שבו המסמך נוצר בסדר יורד (החדש ביותר למעלה).
  • upload_date – התאריך שבו המסמך נוצר בסדר עולה (הישן ביותר למעלה).
  • update_date desc – התאריך שבו המסמך עודכן לאחרונה בסדר יורד (העדכני ביותר למעלה).
  • Update_date – התאריך שבו המסמך עודכן לאחרונה בסדר עולה (הישן ביותר למעלה).

אם לא מציינים מיון, אבל מספקים מילות מפתח לחיפוש, המיון הוא לפי רלוונטיות בסדר יורד (ההתאמות הטובות ביותר מוצגות למעלה). אם לא מציינים את סדר המיון או מילות מפתח, ברירת המחדל היא מיון לפי זמן העדכון בסדר יורד (המסמכים האחרונים מוצגים למעלה).

עימוד

החלוקה לדפים שימושית להצגת נתונים שמתאימים לדף אחד למשתמש הקצה. כאן אפשר לציין את גודל הדף ולקבל ספירה כוללת של גודל התוצאה שתוצג למשתמש (לדוגמה, 'מוצגים 50 מסמכים מתוך 300').

מגדירים בשדה page_size את מספר התוצאות הרצוי שרוצים לקבל עם בקשת החיפוש. יכול להיות שהגודל הזה תואם לדרישות של גודל התצוגה של תוצאות החיפוש בממשק המשתמש.

יש שני מנגנונים: offset וטוקן דף.

ההיסט הוא האינדקס ברשימת המסמכים שניתן להחזיר שרוצים להחזיר. לדוגמה, היסט של 5 אומר שרוצים את המסמך השישי ואילך. כנראה שתגדילו את ההיסט לפי גודל הדף כדי לקבל את דף התוצאות הבא.

לחלופין, אפשר להשתמש באסימון דף ולא צריך לחשב את ההיסט הבא. אחרי ששולחים את בקשת החיפוש הראשונה, מקבלים תשובה עם שדה next_page_token. אם השדה הזה ריק, אין תוצאות נוספות. אם השדה לא ריק, צריך להשתמש בטוקן הזה בבקשת החיפוש הבאה על ידי הגדרת השדה page_token.

בממשקי משתמש מסוימים מוצגת ספירת המסמכים שנמצאו בחיפוש. לדוגמה, you are viewing 10 documents of 120. כדי לקבל את מספר המסמכים שמוחזרים, צריך להגדיר את השדה require_total_size boolean של הבקשה לערך True. טיפ: require_total_size=True גורר קנס על ביצועים. מגדירים את הערך הזה בשאילתת הדף הראשון, ואז מגדירים אותו ל-false בכל הבקשות הבאות, תוך שמירה של הספירה הכוללת במשתנה מקומי.

דוגמאות קוד

Python

מידע נוסף מופיע במאמרי העזרה של Document AI Warehouse Python API.

כדי לבצע אימות ב-Document AI Warehouse, צריך להגדיר את Application Default Credentials. מידע נוסף זמין במאמר הגדרת אימות לסביבת פיתוח מקומית.


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

מידע נוסף מופיע במאמרי העזרה של Document AI Warehouse Java API.

כדי לבצע אימות ב-Document AI Warehouse, צריך להגדיר את Application Default Credentials. מידע נוסף זמין במאמר הגדרת אימות לסביבת פיתוח מקומית.

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

השלבים הבאים