1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/query_parser/snippet.h"
9 #include "base/strings/string_split.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "testing/gtest/include/gtest/gtest.h"
14 namespace query_parser
{
17 // A sample document to compute snippets of.
18 // The \x bits after the first "Google" are UTF-8 of U+2122 TRADE MARK SIGN,
19 // and are useful for verifying we don't screw up in UTF-8/UTF-16 conversion.
20 const char* kSampleDocument
= "Google\xe2\x84\xa2 Terms of Service "
22 "1. Your relationship with Google "
23 "1.1 Your use of Google's products, software, services and web sites "
24 "(referred to collectively as the \"Services\" in this document and excluding "
25 "any services provided to you by Google under a separate written agreement) "
26 "is subject to the terms of a legal agreement between you and Google. "
27 "\"Google\" means Google Inc., whose principal place of business is at 1600 "
28 "Amphitheatre Parkway, Mountain View, CA 94043, United States. This document "
29 "explains how the agreement is made up, and sets out some of the terms of "
33 // Thai sample taken from http://www.google.co.th/intl/th/privacy.html
34 // TODO(jungshik) : Add more samples (e.g. Hindi) after porting
35 // ICU 4.0's character iterator changes to our copy of ICU 3.8 to get
36 // grapheme clusters in Indic scripts handled more reasonably.
37 const char* kThaiSample
= "Google \xE0\xB9\x80\xE0\xB8\x81\xE0\xB9\x87"
38 "\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7"
39 "\xE0\xB8\xA1 \xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
40 "\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99\xE0\xB8\x9A"
41 "\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5 \xE0\xB9\x80\xE0\xB8\xA1"
42 "\xE0\xB8\xB7\xE0\xB9\x88\xE0\xB8\xAD\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93"
43 "\xE0\xB8\xA5\xE0\xB8\x87\xE0\xB8\x97\xE0\xB8\xB0\xE0\xB9\x80\xE0\xB8\x9A"
44 "\xE0\xB8\xB5\xE0\xB8\xA2\xE0\xB8\x99\xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7"
45 "\xE0\xB9\x88\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89\xE0\xB8\x9A"
46 "\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\x82"
47 "\xE0\xB8\xAD\xE0\xB8\x87 Google \xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB7"
48 "\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89\xE0\xB8\x82\xE0\xB9\x89"
49 "\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5\xE0\xB8\x94\xE0\xB8\xB1"
50 "\xE0\xB8\x87\xE0\xB8\x81\xE0\xB8\xA5\xE0\xB9\x88\xE0\xB8\xB2\xE0\xB8\xA7"
51 "\xE0\xB9\x82\xE0\xB8\x94\xE0\xB8\xA2\xE0\xB8\xAA\xE0\xB8\xA1\xE0\xB8\xB1"
52 "\xE0\xB8\x84\xE0\xB8\xA3\xE0\xB9\x83\xE0\xB8\x88 \xE0\xB9\x80\xE0\xB8\xA3"
53 "\xE0\xB8\xB2\xE0\xB8\xAD\xE0\xB8\xB2\xE0\xB8\x88\xE0\xB8\xA3\xE0\xB8\xA7"
54 "\xE0\xB8\xA1\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
55 "\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99\xE0\xB8\x9A"
56 "\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5\xE0\xB8\x97\xE0\xB8\xB5"
57 "\xE0\xB9\x88\xE0\xB9\x80\xE0\xB8\x81\xE0\xB9\x87\xE0\xB8\x9A\xE0\xB8\xA3"
58 "\xE0\xB8\xA7\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1\xE0\xB8\x88"
59 "\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93\xE0\xB9\x80"
60 "\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\xB1\xE0\xB8\x9A"
61 "\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5"
62 "\xE0\xB8\x88\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4"
63 "\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\xAD\xE0\xB8\xB7\xE0\xB9\x88"
64 "\xE0\xB8\x99\xE0\xB8\x82\xE0\xB8\xAD\xE0\xB8\x87 Google \xE0\xB8\xAB"
65 "\xE0\xB8\xA3\xE0\xB8\xB7\xE0\xB8\xAD\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84"
66 "\xE0\xB8\x84\xE0\xB8\xA5\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\xAA"
67 "\xE0\xB8\xB2\xE0\xB8\xA1 \xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7\xE0\xB9\x88"
68 "\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89\xE0\xB8\x9C\xE0\xB8\xB9"
69 "\xE0\xB9\x89\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89\xE0\xB9\x84\xE0\xB8\x94"
70 "\xE0\xB9\x89\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x9B\xE0\xB8\xA3"
71 "\xE0\xB8\xB0\xE0\xB8\xAA\xE0\xB8\x9A\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3"
72 "\xE0\xB8\x93\xE0\xB9\x8C\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\x94"
73 "\xE0\xB8\xB5\xE0\xB8\x82\xE0\xB8\xB6\xE0\xB9\x89\xE0\xB8\x99 \xE0\xB8\xA3"
74 "\xE0\xB8\xA7\xE0\xB8\xA1\xE0\xB8\x97\xE0\xB8\xB1\xE0\xB9\x89\xE0\xB8\x87"
75 "\xE0\xB8\x9B\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB9\x81\xE0\xB8\x95"
76 "\xE0\xB9\x88\xE0\xB8\x87\xE0\xB9\x80\xE0\xB8\x99\xE0\xB8\xB7\xE0\xB9\x89"
77 "\xE0\xB8\xAD\xE0\xB8\xAB\xE0\xB8\xB2\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89"
78 "\xE0\xB9\x80\xE0\xB8\xAB\xE0\xB8\xA1\xE0\xB8\xB2\xE0\xB8\xB0\xE0\xB8\xAA"
79 "\xE0\xB8\xB3\xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x84"
80 "\xE0\xB8\xB8\xE0\xB8\x93";
82 // Comparator for sorting by the first element in a pair.
83 bool ComparePair1st(const Snippet::MatchPosition
& a
,
84 const Snippet::MatchPosition
& b
) {
85 return a
.first
< b
.first
;
88 // For testing, we'll compute the match positions manually instead of using
89 // sqlite's FTS matching. BuildSnippet returns the snippet for matching
90 // |query| against |document|. Matches are surrounded by "**".
91 base::string16
BuildSnippet(const std::string
& document
,
92 const std::string
& query
) {
93 // This function assumes that |document| does not contain
94 // any character for which lowercasing changes its length. Further,
95 // it's assumed that lowercasing only the ASCII-portion works for
96 // |document|. We need to add more test cases and change this function
97 // to be more generic depending on how we deal with 'folding for match'
99 const std::string document_folded
= base::StringToLowerASCII(document
);
101 // Manually construct match_positions of the document.
102 Snippet::MatchPositions match_positions
;
103 match_positions
.clear();
104 for (const std::string
& word
: base::SplitString(
105 query
, " ", base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
)) {
106 // Insert all instances of this word into match_pairs.
108 while ((ofs
= document_folded
.find(word
, ofs
)) != std::string::npos
) {
109 match_positions
.push_back(std::make_pair(ofs
, ofs
+ word
.size()));
113 // Sort match_positions in order of increasing offset.
114 std::sort(match_positions
.begin(), match_positions
.end(), ComparePair1st
);
116 // Compute the snippet.
118 snippet
.ComputeSnippet(match_positions
, document
);
120 // Now "highlight" all matches in the snippet with **.
121 base::string16 star_snippet
;
122 Snippet::MatchPositions::const_iterator match
;
124 for (match
= snippet
.matches().begin();
125 match
!= snippet
.matches().end(); ++match
) {
126 star_snippet
+= snippet
.text().substr(pos
, match
->first
- pos
);
127 star_snippet
+= base::UTF8ToUTF16("**");
128 star_snippet
+= snippet
.text().substr(match
->first
,
129 match
->second
- match
->first
);
130 star_snippet
+= base::UTF8ToUTF16("**");
133 star_snippet
+= snippet
.text().substr(pos
);
138 TEST(Snippets
, SimpleQuery
) {
139 ASSERT_EQ(" ... eferred to collectively as the \"Services\" in this "
140 "**document** and excluding any services provided to you by "
141 "Goo ... ... way, Mountain View, CA 94043, United States. This "
142 "**document** explains how the agreement is made up, and sets "
144 base::UTF16ToUTF8(BuildSnippet(kSampleDocument
, "document")));
147 // Test that two words that are near each other don't produce two elided bits.
148 TEST(Snippets
, NearbyWords
) {
149 ASSERT_EQ(" ... lace of business is at 1600 Amphitheatre Parkway, "
150 "**Mountain** **View**, CA 94043, United States. This "
151 "document explains ... ",
152 base::UTF16ToUTF8(BuildSnippet(kSampleDocument
, "mountain view")));
155 // The above tests already test that we get byte offsets correct, but here's
156 // one that gets the "TM" in its snippet.
157 TEST(Snippets
, UTF8
) {
158 ASSERT_EQ(" ... ogle\xe2\x84\xa2 Terms of Service Welcome to Google! "
159 "1. Your **relationship** with Google 1.1 Your use of Google's "
161 base::UTF16ToUTF8(BuildSnippet(kSampleDocument
, "relationship")));
164 TEST(Snippets
, ThaiUTF8
) {
165 // There are 3 instances of '\u0E43\u0E2B\u0E49'
166 // (\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89) in kThaiSample.
167 // The 1st is more than |kSniipetContext| graphemes away from the
168 // 2nd while the 2nd and 3rd are within that window. However, with
169 // the 2nd match added, the snippet goes over the size limit so that
170 // the snippet ends right before the 3rd match.
172 "\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
173 "\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99"
174 "\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5 "
175 "\xE0\xB9\x80\xE0\xB8\xA1\xE0\xB8\xB7\xE0\xB9\x88\xE0\xB8\xAD"
176 "\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93\xE0\xB8\xA5\xE0\xB8\x87"
177 "\xE0\xB8\x97\xE0\xB8\xB0\xE0\xB9\x80\xE0\xB8\x9A\xE0\xB8\xB5"
178 "\xE0\xB8\xA2\xE0\xB8\x99\xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7"
179 "\xE0\xB9\x88\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89"
180 "\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB8\x81\xE0\xB8\xB2"
181 "\xE0\xB8\xA3\xE0\xB8\x82\xE0\xB8\xAD\xE0\xB8\x87 Google "
182 "\xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB7\xE0\xB8\xAD**\xE0\xB9\x83"
183 "\xE0\xB8\xAB\xE0\xB9\x89**\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD"
184 "\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5\xE0\xB8\x94\xE0\xB8\xB1"
185 "\xE0\xB8\x87\xE0\xB8\x81\xE0\xB8\xA5\xE0\xB9\x88\xE0\xB8\xB2"
186 "\xE0\xB8\xA7\xE0\xB9\x82\xE0\xB8\x94\xE0\xB8\xA2\xE0\xB8\xAA"
187 "\xE0\xB8\xA1\xE0\xB8\xB1\xE0\xB8\x84\xE0\xB8\xA3\xE0\xB9\x83"
188 "\xE0\xB8\x88 \xE0\xB9\x80\xE0\xB8\xA3\xE0\xB8\xB2\xE0\xB8\xAD"
189 "\xE0\xB8\xB2\xE0\xB8\x88\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1"
190 "\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
191 "\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99"
192 "\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5"
193 "\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB9\x80\xE0\xB8\x81"
194 "\xE0\xB9\x87\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\x9A"
195 "\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1 ... ... \xE0\xB8\x88"
196 "\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93"
197 "\xE0\xB9\x80\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xB2\xE0\xB8\x81"
198 "\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD"
199 "\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5\xE0\xB8\x88\xE0\xB8\xB2"
200 "\xE0\xB8\x81\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB8\x81"
201 "\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\xAD\xE0\xB8\xB7\xE0\xB9\x88"
202 "\xE0\xB8\x99\xE0\xB8\x82\xE0\xB8\xAD\xE0\xB8\x87 Google "
203 "\xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB7\xE0\xB8\xAD\xE0\xB8\x9A"
204 "\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5\xE0\xB8\x97"
205 "\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\xAA\xE0\xB8\xB2\xE0\xB8\xA1 "
206 "\xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7\xE0\xB9\x88\xE0\xB8\xAD**"
207 "\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89**\xE0\xB8\x9C\xE0\xB8\xB9"
208 "\xE0\xB9\x89\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89\xE0\xB9\x84"
209 "\xE0\xB8\x94\xE0\xB9\x89\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A"
210 "\xE0\xB8\x9B\xE0\xB8\xA3\xE0\xB8\xB0\xE0\xB8\xAA\xE0\xB8\x9A"
211 "\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\x93\xE0\xB9\x8C"
212 "\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\x94\xE0\xB8\xB5"
213 "\xE0\xB8\x82\xE0\xB8\xB6\xE0\xB9\x89\xE0\xB8\x99 \xE0\xB8\xA3"
214 "\xE0\xB8\xA7\xE0\xB8\xA1\xE0\xB8\x97\xE0\xB8\xB1\xE0\xB9\x89"
215 "\xE0\xB8\x87\xE0\xB8\x9B\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A"
216 "\xE0\xB9\x81\xE0\xB8\x95\xE0\xB9\x88\xE0\xB8\x87\xE0\xB9\x80"
217 "\xE0\xB8\x99\xE0\xB8\xB7\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xAB"
219 base::UTF16ToUTF8(BuildSnippet(kThaiSample
,
220 "\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89")));
223 TEST(Snippets
, ExtractMatchPositions
) {
225 const std::string offsets_string
;
226 const size_t expected_match_count
;
227 const size_t expected_matches
[10];
229 { "0 0 1 2 0 0 4 1 0 0 1 5", 1, { 1, 6 } },
230 { "0 0 1 4 0 0 2 1", 1, { 1, 5 } },
231 { "0 0 4 1 0 0 2 1", 2, { 2, 3, 4, 5 } },
232 { "0 0 0 1", 1, { 0, 1 } },
233 { "0 0 0 1 0 0 0 2", 1, { 0, 2 } },
234 { "0 0 1 1 0 0 1 2", 1, { 1, 3 } },
235 { "0 0 1 2 0 0 4 3 0 0 3 1", 1, { 1, 7 } },
236 { "0 0 1 4 0 0 2 5", 1, { 1, 7 } },
237 { "0 0 1 2 0 0 1 1", 1, { 1, 3 } },
238 { "0 0 1 1 0 0 5 2 0 0 10 1 0 0 3 10", 2, { 1, 2, 3, 13 } },
240 for (size_t i
= 0; i
< arraysize(data
); ++i
) {
241 Snippet::MatchPositions matches
;
242 Snippet::ExtractMatchPositions(data
[i
].offsets_string
, "0", &matches
);
243 EXPECT_EQ(data
[i
].expected_match_count
, matches
.size());
244 for (size_t j
= 0; j
< data
[i
].expected_match_count
; ++j
) {
245 EXPECT_EQ(data
[i
].expected_matches
[2 * j
], matches
[j
].first
);
246 EXPECT_EQ(data
[i
].expected_matches
[2 * j
+ 1], matches
[j
].second
);
251 } // namespace query_parser