Ticket #52: revpostlist.patch
File revpostlist.patch, 22.4 KB (added by , 18 years ago) |
---|
-
backends/quartz/quartz_database.cc
RCS file: /usr/data/cvs/xapian/xapian-core/backends/quartz/quartz_database.cc,v retrieving revision 1.145 diff -p -u -r1.145 quartz_database.cc
QuartzDatabase::do_open_post_list(const 585 585 tname)); 586 586 } 587 587 588 LeafPostList * 589 QuartzDatabase::do_open_post_list_rev(const string& tname) const 590 { 591 DEBUGCALL(DB, LeafPostList *, "QuartzDatabase::do_open_post_list_rev", 592 tname); 593 Assert(!tname.empty()); 594 595 Xapian::Internal::RefCntPtr<const QuartzDatabase> ptrtothis(this); 596 return(new QuartzRevPostList(ptrtothis, 597 &postlist_table, 598 &positionlist_table, 599 tname)); 600 } 601 588 602 LeafTermList * 589 603 QuartzDatabase::open_term_list(Xapian::docid did) const 590 604 { … … QuartzWritableDatabase::do_open_post_lis 1102 1116 &database_ro.postlist_table, 1103 1117 &database_ro.positionlist_table, 1104 1118 tname)); 1119 } 1120 1121 LeafPostList * 1122 QuartzWritableDatabase::do_open_post_list_rev(const string& tname) const 1123 { 1124 DEBUGCALL(DB, LeafPostList *, "QuartzWritableDatabase::do_open_post_list_rev", tname); 1125 Assert(!tname.empty()); 1126 1127 // Need to flush iff we've got buffered changes to this term's postlist. 1128 map<string, map<docid, pair<char, termcount> > >::const_iterator j; 1129 j = mod_plists.find(tname); 1130 if (j != mod_plists.end()) do_flush_const(); 1131 1132 Xapian::Internal::RefCntPtr<const QuartzWritableDatabase> ptrtothis(this); 1133 return(new QuartzRevPostList(ptrtothis, 1134 &database_ro.postlist_table, 1135 &database_ro.positionlist_table, 1136 tname)); 1105 1137 } 1106 1138 1107 1139 LeafTermList * -
backends/quartz/quartz_database.h
RCS file: /usr/data/cvs/xapian/xapian-core/backends/quartz/quartz_database.h,v retrieving revision 1.78 diff -p -u -r1.78 quartz_database.h
class QuartzDatabase : public Xapian::Da 210 210 bool term_exists(const string & tname) const; 211 211 212 212 LeafPostList * do_open_post_list(const string & tname) const; 213 LeafPostList * do_open_post_list_rev(const string & tname) const; 213 214 LeafTermList * open_term_list(Xapian::docid did) const; 214 215 Xapian::Document::Internal * open_document(Xapian::docid did, bool lazy = false) const; 215 216 PositionList * open_position_list(Xapian::docid did, … … class QuartzWritableDatabase : public Xa 289 290 bool term_exists(const string & tname) const; 290 291 291 292 LeafPostList * do_open_post_list(const string & tname) const; 293 LeafPostList * do_open_post_list_rev(const string & tname) const; 292 294 LeafTermList * open_term_list(Xapian::docid did) const; 293 295 Xapian::Document::Internal * open_document(Xapian::docid did, bool lazy = false) const; 294 296 PositionList * open_position_list(Xapian::docid did, -
backends/quartz/quartz_postlist.cc
RCS file: /usr/data/cvs/xapian/xapian-core/backends/quartz/quartz_postlist.cc,v retrieving revision 1.82 diff -p -u -r1.82 quartz_postlist.cc
PostlistChunkWriter::flush(Btree *table) 604 604 } 605 605 } 606 606 607 /*****************************************************************************/ 608 607 609 /** Read the number of entries in the posting list. 608 610 * This must only be called when *posptr is pointing to the start of 609 611 * the first chunk of the posting list. … … QuartzPostList::get_description() const 913 915 { 914 916 return tname + ":" + om_tostring(number_of_entries); 915 917 } 918 919 /*****************************************************************************/ 920 921 QuartzRevPostList::QuartzRevPostList(Xapian::Internal::RefCntPtr<const Xapian::Database::Internal> this_db_, 922 const Btree * table_, 923 const Btree * positiontable_, 924 const string & tname_) 925 : this_db(this_db_), 926 table(table_), 927 positiontable(positiontable_), 928 tname(tname_), 929 cursor(table->cursor_get()), 930 is_at_end(false), 931 have_started(false) 932 { 933 DEBUGCALL(DB, void, "QuartzRevPostList::QuartzRevPostList", 934 this_db_.get() << ", " << table_ << ", " << 935 positiontable_ << ", " << tname_); 936 string key; 937 make_key(tname, key); 938 int found = cursor->find_entry(key); 939 if (!found) { 940 number_of_entries = 0; 941 collection_freq = 0; 942 is_at_end = true; 943 pos = 0; 944 end = 0; 945 first_did_in_chunk = 0; 946 last_did_in_chunk = 0; 947 return; 948 } 949 cursor->read_tag(); 950 pos = cursor->current_tag.data(); 951 end = pos + cursor->current_tag.size(); 952 953 did = read_start_of_first_chunk(&pos, end, 954 &number_of_entries, &collection_freq); 955 first_did_in_chunk = did; 956 last_did_in_chunk = read_start_of_chunk(&pos, end, first_did_in_chunk, 957 &is_last_chunk); 958 read_wdf_and_length(&pos, end, &wdf, &doclength); 959 if (!is_last_chunk) { 960 make_key(tname, (Xapian::docid)-1, key); 961 (void) cursor->find_entry(key); 962 const char * keypos = cursor->current_key.data(); 963 const char * keyend = keypos + cursor->current_key.size(); 964 if (!check_tname_in_key_lite(&keypos, keyend, tname)) { 965 throw Xapian::DatabaseCorruptError("Chunk for wrong term"); 966 } 967 968 if (!unpack_uint_preserving_sort(&keypos, keyend, &first_did_in_chunk)) { 969 report_read_error(keypos); 970 } 971 972 cursor->read_tag(); 973 pos = cursor->current_tag.data(); 974 end = pos + cursor->current_tag.size(); 975 976 last_did_in_chunk = read_start_of_chunk(&pos, end, first_did_in_chunk, 977 &is_last_chunk); 978 read_wdf_and_length(&pos, end, &wdf, &doclength); 979 } 980 981 buffer.clear(); 982 did = first_did_in_chunk; 983 while (pos != end) { 984 buffer.push_back(PLEntry(did, wdf, doclength)); 985 read_did_increase(&pos, end, &did); 986 read_wdf_and_length(&pos, end, &wdf, &doclength); 987 // Either not at last doc in chunk, or pos == end, but not both. 988 Assert(did <= last_did_in_chunk); 989 Assert(did < last_did_in_chunk || pos == end); 990 Assert(pos != end || did == last_did_in_chunk); 991 } 992 } 993 994 QuartzRevPostList::~QuartzRevPostList() 995 { 996 DEBUGCALL(DB, void, "QuartzRevPostList::~QuartzRevPostList", ""); 997 } 998 999 bool 1000 QuartzRevPostList::next_in_chunk() 1001 { 1002 DEBUGCALL(DB, bool, "QuartzRevPostList::next_in_chunk", ""); 1003 if (buffer.empty()) RETURN(false); 1004 1005 did = buffer.back().did; 1006 wdf = buffer.back().wdf; 1007 doclength = buffer.back().doclength; 1008 buffer.pop_back(); 1009 1010 RETURN(true); 1011 } 1012 1013 void 1014 QuartzRevPostList::next_chunk() 1015 { 1016 DEBUGCALL(DB, void, "QuartzRevPostList::next_chunk", ""); 1017 if (is_last_chunk) { 1018 is_at_end = true; 1019 return; 1020 } 1021 1022 cursor->prev(); 1023 const char * keypos = cursor->current_key.data(); 1024 const char * keyend = keypos + cursor->current_key.size(); 1025 if (!check_tname_in_key_lite(&keypos, keyend, tname)) { 1026 throw Xapian::DatabaseCorruptError("Chunk for wrong term"); 1027 } 1028 1029 if (keypos == keyend) { 1030 is_last_chunk = true; 1031 first_did_in_chunk = read_start_of_first_chunk(&pos, end, 1032 &number_of_entries, &collection_freq); 1033 } else { 1034 if (!unpack_uint_preserving_sort(&keypos, keyend, &first_did_in_chunk)) { 1035 report_read_error(keypos); 1036 } 1037 1038 cursor->read_tag(); 1039 pos = cursor->current_tag.data(); 1040 end = pos + cursor->current_tag.size(); 1041 1042 last_did_in_chunk = read_start_of_chunk(&pos, end, first_did_in_chunk, 1043 &is_last_chunk); 1044 } 1045 read_wdf_and_length(&pos, end, &wdf, &doclength); 1046 buffer.clear(); 1047 did = first_did_in_chunk; 1048 while (pos != end) { 1049 buffer.push_back(PLEntry(did, wdf, doclength)); 1050 read_did_increase(&pos, end, &did); 1051 read_wdf_and_length(&pos, end, &wdf, &doclength); 1052 // Either not at last doc in chunk, or pos == end, but not both. 1053 Assert(did <= last_did_in_chunk); 1054 Assert(did < last_did_in_chunk || pos == end); 1055 Assert(pos != end || did == last_did_in_chunk); 1056 } 1057 } 1058 1059 PositionList * 1060 QuartzRevPostList::read_position_list() 1061 { 1062 DEBUGCALL(DB, PositionList *, "QuartzRevPostList::read_position_list", ""); 1063 1064 positionlist.read_data(positiontable, did, tname); 1065 1066 RETURN(&positionlist); 1067 } 1068 1069 PositionList * 1070 QuartzRevPostList::open_position_list() const 1071 { 1072 DEBUGCALL(DB, PositionList *, "QuartzRevPostList::open_position_list", ""); 1073 1074 AutoPtr<QuartzPositionList> poslist(new QuartzPositionList()); 1075 poslist->read_data(positiontable, did, tname); 1076 1077 RETURN(poslist.release()); 1078 } 1079 1080 PostList * 1081 QuartzRevPostList::next(Xapian::weight w_min) 1082 { 1083 DEBUGCALL(DB, PostList *, "QuartzRevPostList::next", w_min); 1084 (void)w_min; // no warning 1085 1086 if (!have_started) { 1087 have_started = true; 1088 } else { 1089 if (!next_in_chunk()) next_chunk(); 1090 } 1091 1092 DEBUGLINE(DB, string("Moved to ") << 1093 (is_at_end ? string("end.") : string("docid, wdf, doclength = ") + 1094 om_tostring(did) + ", " + om_tostring(wdf) + ", " + 1095 om_tostring(doclength))); 1096 1097 RETURN(NULL); 1098 } 1099 1100 bool 1101 QuartzRevPostList::current_chunk_contains(Xapian::docid desired_did) 1102 { 1103 DEBUGCALL(DB, bool, "QuartzRevPostList::current_chunk_contains", desired_did); 1104 if (desired_did >= first_did_in_chunk && 1105 desired_did <= last_did_in_chunk) { 1106 RETURN(true); 1107 } 1108 RETURN(false); 1109 } 1110 1111 void 1112 QuartzRevPostList::move_to_chunk_containing(Xapian::docid desired_did) 1113 { 1114 DEBUGCALL(DB, void, 1115 "QuartzRevPostList::move_to_chunk_containing", desired_did); 1116 string key; 1117 make_key(tname, desired_did, key); 1118 (void) cursor->find_entry(key); 1119 Assert(!cursor->after_end()); 1120 1121 const char * keypos = cursor->current_key.data(); 1122 const char * keyend = keypos + cursor->current_key.size(); 1123 // Check we're still in same postlist 1124 if (!check_tname_in_key_lite(&keypos, keyend, tname)) { 1125 // This should only happen if the postlist doesn't exist at all. 1126 is_at_end = true; 1127 is_last_chunk = true; 1128 return; 1129 } 1130 is_at_end = false; 1131 1132 cursor->read_tag(); 1133 pos = cursor->current_tag.data(); 1134 end = pos + cursor->current_tag.size(); 1135 1136 if (keypos == keyend) { 1137 // In first chunk 1138 #ifdef XAPIAN_DEBUG 1139 Xapian::termcount old_number_of_entries = number_of_entries; 1140 Xapian::termcount old_collection_freq = collection_freq; 1141 first_did_in_chunk = read_start_of_first_chunk(&pos, end, &number_of_entries, 1142 &collection_freq); 1143 Assert(old_number_of_entries == number_of_entries); 1144 Assert(old_collection_freq == collection_freq); 1145 #else 1146 first_did_in_chunk = read_start_of_first_chunk(&pos, end, 0, 0); 1147 #endif 1148 } else { 1149 // In normal chunk 1150 if (!unpack_uint_preserving_sort(&keypos, keyend, &first_did_in_chunk)) { 1151 report_read_error(keypos); 1152 } 1153 } 1154 1155 last_did_in_chunk = read_start_of_chunk(&pos, end, first_did_in_chunk, 1156 &is_last_chunk); 1157 read_wdf_and_length(&pos, end, &wdf, &doclength); 1158 buffer.clear(); 1159 did = first_did_in_chunk; 1160 while (pos != end) { 1161 buffer.push_back(PLEntry(did, wdf, doclength)); 1162 read_did_increase(&pos, end, &did); 1163 read_wdf_and_length(&pos, end, &wdf, &doclength); 1164 // Either not at last doc in chunk, or pos == end, but not both. 1165 Assert(did <= last_did_in_chunk); 1166 Assert(did < last_did_in_chunk || pos == end); 1167 Assert(pos != end || did == last_did_in_chunk); 1168 } 1169 } 1170 1171 bool 1172 QuartzRevPostList::move_forward_in_chunk_to_at_least(Xapian::docid desired_did) 1173 { 1174 DEBUGCALL(DB, bool, 1175 "QuartzRevPostList::move_forward_in_chunk_to_at_least", desired_did); 1176 if (desired_did > last_did_in_chunk) { 1177 pos = end; 1178 RETURN(false); 1179 } 1180 while (did < desired_did) { 1181 // FIXME: perhaps we don't need to decode the wdf and document length 1182 // for documents we're skipping past. 1183 bool at_end_of_chunk = !next_in_chunk(); 1184 if (at_end_of_chunk) RETURN(false); 1185 } 1186 RETURN(true); 1187 } 1188 1189 PostList * 1190 QuartzRevPostList::skip_to(Xapian::docid desired_did, Xapian::weight w_min) 1191 { 1192 DEBUGCALL(DB, PostList *, 1193 "QuartzRevPostList::skip_to", desired_did << ", " << w_min); 1194 (void)w_min; // no warning 1195 // We've started now - if we hadn't already, we're already positioned 1196 // at start so there's no need to actually do anything. 1197 have_started = true; 1198 1199 // Don't skip back, and don't need to do anything if already there. 1200 if (desired_did <= did) RETURN(NULL); 1201 1202 // Move to correct chunk 1203 if (!current_chunk_contains(desired_did)) { 1204 move_to_chunk_containing(desired_did); 1205 // Might be at_end now - this is why we need the test before moving 1206 // forward in chunk. 1207 } 1208 1209 // Move to correct position in chunk 1210 if (!is_at_end) { 1211 #ifdef XAPIAN_DEBUG 1212 bool have_document = 1213 #else 1214 (void) 1215 #endif 1216 move_forward_in_chunk_to_at_least(desired_did); 1217 Assert(have_document); 1218 } 1219 1220 DEBUGLINE(DB, string("Skipped to ") << 1221 (is_at_end ? string("end.") : string("docid, wdf, doclength = ") + 1222 om_tostring(did) + ", " + om_tostring(wdf) + ", " + 1223 om_tostring(doclength) + ".")); 1224 1225 RETURN(NULL); 1226 } 1227 1228 string 1229 QuartzRevPostList::get_description() const 1230 { 1231 return tname + ":" + om_tostring(number_of_entries); 1232 } 1233 1234 /*****************************************************************************/ 916 1235 917 1236 // Returns the last did to allow in this chunk. 918 1237 Xapian::docid -
backends/quartz/quartz_postlist.h
RCS file: /usr/data/cvs/xapian/xapian-core/backends/quartz/quartz_postlist.h,v retrieving revision 1.33 diff -p -u -r1.33 quartz_postlist.h
class QuartzPostList : public LeafPostLi 247 247 Xapian::termcount * collection_freq_ptr); 248 248 }; 249 249 250 /** A postlist in a quartz database. 251 */ 252 class QuartzRevPostList : public LeafPostList { 253 private: 254 /** The database we are searching. This pointer is held so that the 255 * database doesn't get deleted before us. 256 */ 257 Xapian::Internal::RefCntPtr<const Xapian::Database::Internal> this_db; 258 259 /// The table containing the postlist. 260 const Btree * table; 261 262 /// The table containing positionlists. 263 const Btree * positiontable; 264 265 /// The termname for this postlist. 266 string tname; 267 268 /// Cursor pointing to current chunk of postlist. 269 AutoPtr<Bcursor> cursor; 270 271 /// True if this is the last chunk. 272 bool is_last_chunk; 273 274 /// The first document id in this chunk. 275 Xapian::docid first_did_in_chunk; 276 277 /// The last document id in this chunk. 278 Xapian::docid last_did_in_chunk; 279 280 /// Position of iteration through current chunk. 281 const char * pos; 282 283 /// Pointer to byte after end of current chunk. 284 const char * end; 285 286 /// Document id we're currently at. 287 Xapian::docid did; 288 289 /// The (absolute) length of the current document. 290 quartz_doclen_t doclength; 291 292 /// The wdf of the current document. 293 Xapian::termcount wdf; 294 295 /// Whether we've run off the end of the list yet. 296 bool is_at_end; 297 298 /// Whether we've started reading the list yet. 299 bool have_started; 300 301 /// The number of entries in the posting list. 302 Xapian::doccount number_of_entries; 303 304 /// The number of occurrences of the term in the posting list. 305 Xapian::termcount collection_freq; 306 307 /// The position list object for this posting list. 308 QuartzPositionList positionlist; 309 310 struct PLEntry { 311 Xapian::docid did; 312 Xapian::doccount wdf; 313 Xapian::termcount doclength; 314 PLEntry(Xapian::docid did_, Xapian::termcount wdf_, quartz_doclen_t doclength_) : did(did_), wdf(wdf_), doclength(doclength_) { } 315 }; 316 317 vector<PLEntry> buffer; 318 319 /// Copying is not allowed. 320 QuartzRevPostList(const QuartzRevPostList &); 321 322 /// Assignment is not allowed. 323 void operator=(const QuartzRevPostList &); 324 325 /** Move to the next item in the chunk, if possible. 326 * If already at the end of the chunk, returns false. 327 */ 328 bool next_in_chunk(); 329 330 /** Move to the next chunk. 331 * 332 * If there are no more chunks in this postlist, this will set 333 * is_at_end to true. 334 */ 335 void next_chunk(); 336 337 /** Return true if the given document ID lies in the range covered 338 * by the current chunk. This does not say whether the document ID 339 * is actually present. It will return false if the document ID 340 * is greater than the last document ID in the chunk, even if it is 341 * less than the first document ID in the next chunk: it is possible 342 * for no chunk to contain a particular document ID. 343 */ 344 bool current_chunk_contains(Xapian::docid desired_did); 345 346 /** Move to chunk containing the specified document ID. 347 * 348 * This moves to the chunk whose starting document ID is 349 * <= desired_did, but such that the next chunk's starting 350 * document ID is > desired_did. 351 * 352 * It is thus possible that current_chunk_contains(desired_did) 353 * will return false after this call, since the document ID 354 * might lie after the end of this chunk, but before the start 355 * of the next chunk. 356 */ 357 void move_to_chunk_containing(Xapian::docid desired_did); 358 359 /** Scan forward in the current chunk for the specified document ID. 360 * 361 * This is particularly efficient if the desired document ID is 362 * greater than the last in the chunk - it then skips straight 363 * to the end. 364 * 365 * @return true if we moved to a valid document, 366 * false if we reached the end of the chunk. 367 */ 368 bool move_forward_in_chunk_to_at_least(Xapian::docid desired_did); 369 370 public: 371 /// Default constructor. 372 QuartzRevPostList(Xapian::Internal::RefCntPtr<const Xapian::Database::Internal> this_db_, 373 const Btree * table_, 374 const Btree * positiontable_, 375 const string & tname); 376 377 /// Destructor. 378 ~QuartzRevPostList(); 379 380 /** Returns number of docs indexed by this term. 381 * 382 * This is the length of the postlist. 383 */ 384 Xapian::doccount get_termfreq() const { return number_of_entries; } 385 386 /** Returns the number of occurrences of the term in the database. 387 * 388 * This is the sum of the wdfs in the postlist. 389 */ 390 Xapian::termcount get_collection_freq() const { return collection_freq; } 391 392 /// Returns the current docid. 393 Xapian::docid get_docid() const { Assert(have_started); return did; } 394 395 /// Returns the length of current document. 396 Xapian::doclength get_doclength() const { 397 Assert(have_started); 398 return static_cast<Xapian::doclength>(doclength); 399 } 400 401 /** Returns the Within Document Frequency of the term in the current 402 * document. 403 */ 404 Xapian::termcount get_wdf() const { Assert(have_started); return wdf; } 405 406 /** Get the list of positions of the term in the current document. 407 */ 408 PositionList *read_position_list(); 409 410 /** Get the list of positions of the term in the current document. 411 */ 412 PositionList * open_position_list() const; 413 414 /// Move to the next document. 415 PostList * next(Xapian::weight w_min); 416 417 /// Skip to next document with docid >= docid. 418 PostList * skip_to(Xapian::docid desired_did, Xapian::weight w_min); 419 420 /// Return true if and only if we're off the end of the list. 421 bool at_end() const { return is_at_end; } 422 423 /// Get a description of the document. 424 std::string get_description() const; 425 }; 426 250 427 #endif /* OM_HGUARD_QUARTZ_POSTLIST_H */ -
common/database.h
RCS file: /usr/data/cvs/xapian/xapian-core/common/database.h,v retrieving revision 1.138 diff -p -u -r1.138 database.h
class Database::Internal : public Xapian 89 89 * use. 90 90 */ 91 91 virtual LeafPostList * do_open_post_list(const string & tname) const = 0; 92 virtual LeafPostList * do_open_post_list_rev(const string & tname) const = 0; 92 93 93 94 public: 94 95 /** Destroy the database. … … class Database::Internal : public Xapian 189 190 * This object must be deleted by the caller after 190 191 * use. 191 192 */ 192 LeafPostList * open_post_list(const string & tname ) const {193 LeafPostList * open_post_list(const string & tname, bool rev = false) const { 193 194 if (!term_exists(tname)) { 194 195 DEBUGLINE(MATCH, tname + " is not in database."); 195 196 // Term doesn't exist in this database. However, we create … … class Database::Internal : public Xapian 197 198 // cleaner (term might exist in other databases). 198 199 return new EmptyPostList(); 199 200 } 200 return do_open_post_list(tname);201 return rev ? do_open_post_list_rev(tname) : do_open_post_list(tname); 201 202 } 202 203 203 204 /** Open a term list. -
common/multimatch.h
RCS file: /usr/data/cvs/xapian/xapian-core/common/multimatch.h,v retrieving revision 1.75 diff -p -u -r1.75 multimatch.h
class MultiMatch 143 143 * and the maxweight now possible is smaller. 144 144 */ 145 145 void recalc_maxweight(); 146 147 /** Called by LocalSubMatch to find out the sort direction so it 148 * can decide whether to use reverse postlists or not. 149 */ 150 bool get_sort_forward() const { return sort_forward; } 146 151 }; 147 152 148 153 #endif /* OM_HGUARD_MULTIMATCH_H */ -
matcher/localmatch.cc
RCS file: /usr/data/cvs/xapian/xapian-core/matcher/localmatch.cc,v retrieving revision 1.119 diff -p -u -r1.119 localmatch.cc
LocalSubMatch::postlist_from_query(const 410 410 } 411 411 412 412 // MULTI 413 LeafPostList * pl = db->open_post_list(query->tname); 413 LeafPostList * pl; 414 pl = db->open_post_list(query->tname, !matcher->get_sort_forward()); 414 415 pl->set_termweight(wt); 415 416 RETURN(pl); 416 417 } -
matcher/multimatch.cc
RCS file: /usr/data/cvs/xapian/xapian-core/matcher/multimatch.cc,v retrieving revision 1.171 diff -p -u -r1.171 multimatch.cc
MultiMatch::get_mset(Xapian::doccount fi 738 738 if (!sort_bands && items.size() == max_msize) { 739 739 // We're done if this is a forward boolean match 740 740 // (bodgetastic, FIXME better if we can) 741 if (max_weight == 0 && sort_forward) break;741 if (max_weight == 0) break; 742 742 } 743 743 } 744 744 }