Compress features in database

This commit is contained in:
matlabbe
2026-03-23 20:09:01 -07:00
parent 5b985f69be
commit a67876b3eb
7 changed files with 437 additions and 39 deletions

View File

@@ -1513,4 +1513,108 @@ void DBDriver::generateGraph(
}
}
std::vector<unsigned char> DBDriver::serializeFeatures(
const std::vector<cv::KeyPoint> & keypoints,
const std::vector<cv::Point3f> & points3D,
const cv::Mat & descriptors)
{
const int headerSize = 13;
int header[headerSize] = {
RTABMAP_VERSION_MAJOR, RTABMAP_VERSION_MINOR, RTABMAP_VERSION_PATCH, // 0,1,2
CV_MAJOR_VERSION, CV_MINOR_VERSION, CV_SUBMINOR_VERSION, // 3,4,5 (In case the format/order/size of KeyPoint and/or Point3f changes in the future)
sizeof(cv::KeyPoint), keypoints.size(), // 6,7
sizeof(cv::Point3f), points3D.size(), // 8,9
descriptors.type(), descriptors.cols, descriptors.rows} // 10,11,12
UDEBUG("Header: %d %d %d %d %d %d %d %d %d %d %d %d %d",
header[0],header[1],header[2],header[3],header[4],header[5],header[6],header[7],header[8],header[9],header[10],header[11],header[12]);
std::vector<unsigned char> data(
sizeof(int)*headerSize +
keypoints.size()*sizeof(cv::KeyPoint) + // pos_x, pos_y, size, dir, response, octave
points3D.size()*sizeof(cv::Point3f) + // depth_x, depth_y, depth_z
descriptors.total()*descriptors.elemSize());
memcpy(data.data(), header, sizeof(int)*headerSize);
int index = sizeof(int)*headerSize;
if(!keypoints.empty())
{
memcpy(data.data()+index, keypoints.data(), sizeof(cv::KeyPoint)*keypoints.size());
index += sizeof(cv::KeyPoint)*(keypoints.size());
}
if(!points3D.empty())
{
memcpy(data.data()+index, points3D.data(), sizeof(cv::Point3f)*points3D.size());
index += sizeof(cv::KeyPoint)*(points3D.size());
}
if(!descriptors.empty())
{
memcpy(data.data()+index, descriptors.data, descriptors.elemSize()*descriptors.total());
index+=descriptors.elemSize()*(descriptors.total());
}
return compressData(cv::Mat(1, data.size(), CV_8UC1, (void *)data.data()));
}
bool DBDriver::deserializeFeatures(
const unsigned char * compressedData,
unsigned int compressedDataSize,
std::vector<cv::KeyPoint> & keypoints,
std::vector<cv::Point3f> & points3D,
cv::Mat & descriptors)
{
cv::Mat serializedData = uncompressData(compressedData, compressedDataSize);
if(serializedData.empty())
{
return false;
}
UASSERT(serializedData.type() == CV_8UC1);
int headerSize = 13;
if(serializedData.total() >= sizeof(int)*headerSize)
{
const int * header = (const int *)serializedData.data();
UASSERT(header[6] == sizeof(cv::KeyPoint));
int n_kpts = header[7];
UASSERT(header[8] == sizeof(cv::Point3f));
int n_pts = header[9];
int d_type = header[10];
int d_rows = header[11];
int d_cols = header[12];
keypoints.resize(n_kpts);
points3D.resize(n_pts);
descriptors = cv::Mat(d_rows, d_cols, d_type);
unsigned int requiredDataSize = sizeof(int)*headerSize +
sizeof(cv::KeyPoint)*n_kpts +
sizeof(cv::Point3f)*n_pts +
descriptors.total() * descriptors.elemSize();
UASSERT_MSG(serializedData.total() == requiredDataSize,
uFormat("dataSize=%d != required=%d (header: version %d.%d.%d cv=%d.%d.%d kpts=%d (size=%d) pts=%d (size=%d) descriptors=%dx%d type=%d",
serializedData.total(),
requiredDataSize,
header[0], header[1], header[2],
header[3], header[4], header[5],
header[7], header[6],
header[9], header[8],
header[11], header[12], header[10]).c_str());
unsigned int index = sizeof(int)*headerSize;
if(n_kpts != 0)
{
memcpy(keypoints.data(), (void*)(serializedData.data+index), n_kpts*sizeof(cv::KeyPoint));
index += n_kpts*sizeof(cv::KeyPoint);
}
if(n_pts != 0)
{
memcpy(points3D.data(), (void*)(serializedData.data+index), n_pts*sizeof(cv::Point3f));
index += n_pts*sizeof(cv::Point3f);
}
if(d_rows > 0)
{
cv::Mat(d_rows, d_cols, d_type, (void*)(serializedData.data+index)).copyTo(descriptors);
index+=descriptors.elemSize()*(descriptors.total());
}
UASSERT(index == serializedData.size());
return true;
}
UERROR("Wrong serialized features format detected (size in bytes=%ld)! Cannot deserialize the data.", serializedData.size());
return false;
}
} // namespace rtabmap

View File

@@ -1296,12 +1296,12 @@ std::map<int, std::vector<int> > DBDriverSqlite3::getAllStatisticsWmStatesQuery(
return data;
}
void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, bool images, bool scan, bool userData, bool occupancyGrid) const
void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, bool images, bool scan, bool userData, bool occupancyGrid, bool features) const
{
//UDEBUG("load data for %d signatures images=%d scan=%d userData=%d, grid=%d",
// (int)signatures.size(), images?1:0, scan?1:0, userData?1:0, occupancyGrid?1:0);
if(!images && !scan && !userData && !occupancyGrid)
if(!images && !scan && !userData && !occupancyGrid && (uStrNumCmp(_version, "0.24.0") < 0 || !features))
{
UWARN("All requested data fields are false! Nothing loaded...");
return;
@@ -1319,6 +1319,7 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, boo
{
std::stringstream fields;
bool fieldAdded = false;
if(images)
{
if(uStrNumCmp(_version, "0.22.0") >= 0)
@@ -1329,30 +1330,33 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, boo
{
fields << "image, depth, calibration";
}
if(scan || userData || occupancyGrid)
{
fields << ", ";
}
fieldAdded = true;
}
if(scan)
{
fields << "scan_info, scan";
if(userData || occupancyGrid)
if(fieldAdded)
{
fields << ", ";
}
fieldAdded = true;
fields << "scan_info, scan";
}
if(userData)
{
fields << "user_data";
if(occupancyGrid)
if(fieldAdded)
{
fields << ", ";
}
fieldAdded = true;
fields << "user_data";
}
if(occupancyGrid)
{
if(fieldAdded)
{
fields << ", ";
}
fieldAdded = true;
if(uStrNumCmp(_version, "0.16.0") >= 0)
{
fields << "ground_cells, obstacle_cells, empty_cells, cell_size, view_point_x, view_point_y, view_point_z";
@@ -1363,6 +1367,16 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, boo
}
}
if(uStrNumCmp(_version, "0.24.0") >= 0)
{
if(fieldAdded)
{
fields << ", ";
}
fieldAdded = true;
fields << "features";
}
query << "SELECT " << fields.str().c_str() << " "
<< "FROM Data "
<< "WHERE id = ?"
@@ -1830,6 +1844,23 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, boo
viewPoint.z = sqlite3_column_double(ppStmt, index++);
}
// Features
std::vector<cv::KeyPoint> keypoints;
std::vector<cv::Point3f> points3D;
cv::Mat descriptors;
if(uStrNumCmp(_version, "0.24.0") >= 0 && features)
{
data = sqlite3_column_blob(ppStmt, index);
dataSize = sqlite3_column_bytes(ppStmt, index++);
if(dataSize > 0 && data)
{
if(!deserializeFeatures(data, dataSize, keypoints, points3D, descriptors))
{
UERROR("Failed desrializing features for node %d!", (*iter)->id());
}
}
}
if(scan)
{
LaserScan laserScan;
@@ -1864,6 +1895,11 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures, boo
(*iter)->sensorData().setOccupancyGrid(groundCellsCompressed, obstacleCellsCompressed, emptyCellsCompressed, cellSize, viewPoint);
}
if(features)
{
(*iter)->sensorData().setFeatures(keypoints, points3D, descriptors);
}
rc = sqlite3_step(ppStmt); // next result...
}
UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
@@ -3852,6 +3888,12 @@ void DBDriverSqlite3::loadWordsQuery(std::list<Signature *> & signatures) const
sqlite3_stmt * ppStmt = 0;
std::stringstream query;
if(uStrNumCmp(_version, "0.13.0") >= 0)
{
query << "SELECT word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor "
"FROM Feature "
"WHERE node_id = ? ";
}
if(uStrNumCmp(_version, "0.13.0") >= 0)
{
query << "SELECT word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor "
@@ -4600,24 +4642,35 @@ void DBDriverSqlite3::saveQuery(const std::list<Signature *> & signatures)
for(std::list<Signature *>::const_iterator i=signatures.begin(); i!=signatures.end(); ++i)
{
UASSERT((*i)->getWords().size() == (*i)->getWordsKpts().size());
UASSERT((*i)->getWords3().empty() || (*i)->getWords().size() == (*i)->getWords3().size());
UASSERT((*i)->getWordsDescriptors().empty() || (int)(*i)->getWords().size() == (*i)->getWordsDescriptors().rows);
for(std::multimap<int, int>::const_iterator w=(*i)->getWords().begin(); w!=(*i)->getWords().end(); ++w)
if(uStrNumCmp(_version, "0.24.0") >= 0)
{
cv::Point3f pt(nanFloat,nanFloat,nanFloat);
if(!(*i)->getWords3().empty())
// Only node -> word -> index are saved in Feature
for(std::multimap<int, int>::const_iterator w=(*i)->getWords().begin(); w!=(*i)->getWords().end(); ++w)
{
pt = (*i)->getWords3()[w->second];
stepKeypoint(ppStmt, (*i)->id(), w->first, w->second);
}
}
else
{
UASSERT((*i)->getWords3().empty() || (*i)->getWords().size() == (*i)->getWords3().size());
UASSERT((*i)->getWordsDescriptors().empty() || (int)(*i)->getWords().size() == (*i)->getWordsDescriptors().rows);
cv::Mat descriptor;
if(!(*i)->getWordsDescriptors().empty())
for(std::multimap<int, int>::const_iterator w=(*i)->getWords().begin(); w!=(*i)->getWords().end(); ++w)
{
descriptor = (*i)->getWordsDescriptors().row(w->second);
}
cv::Point3f pt(nanFloat,nanFloat,nanFloat);
if(!(*i)->getWords3().empty())
{
pt = (*i)->getWords3()[w->second];
}
stepKeypoint(ppStmt, (*i)->id(), w->first, (*i)->getWordsKpts()[w->second], pt, descriptor);
cv::Mat descriptor;
if(!(*i)->getWordsDescriptors().empty())
{
descriptor = (*i)->getWordsDescriptors().row(w->second);
}
stepKeypoint(ppStmt, (*i)->id(), w->first, (*i)->getWordsKpts()[w->second], pt, descriptor);
}
}
}
// Finalize (delete) the statement
@@ -6410,7 +6463,11 @@ void DBDriverSqlite3::stepScanUpdate(sqlite3_stmt * ppStmt, int nodeId, const La
std::string DBDriverSqlite3::queryStepSensorData() const
{
UASSERT(uStrNumCmp(_version, "0.10.0") >= 0);
if(uStrNumCmp(_version, "0.22.0") >= 0)
if(uStrNumCmp(_version, "0.24.0") >= 0)
{
return "INSERT INTO Data(id, image, depth, depth_confidence, calibration, scan_info, scan, user_data, ground_cells, obstacle_cells, empty_cells, cell_size, view_point_x, view_point_y, view_point_z, features) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
}
else if(uStrNumCmp(_version, "0.22.0") >= 0)
{
return "INSERT INTO Data(id, image, depth, depth_confidence, calibration, scan_info, scan, user_data, ground_cells, obstacle_cells, empty_cells, cell_size, view_point_x, view_point_y, view_point_z) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
}
@@ -6734,6 +6791,22 @@ void DBDriverSqlite3::stepSensorData(sqlite3_stmt * ppStmt,
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
}
if(uStrNumCmp(_version, "0.24.0") >= 0)
{
//features
std::vector<unsigned char> serializedFeatures = serializeFeatures(sensorData.keypoints(), sensorData.keypoints3D(), sensorData.descriptors());
if(serializedFeatures.empty())
{
rc = sqlite3_bind_null(ppStmt, index++);
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
}
else
{
rc = sqlite3_bind_blob(ppStmt, index++, serializedFeatures.data(), (int)serializedFeatures.size(), SQLITE_STATIC);
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
}
}
//step
rc=sqlite3_step(ppStmt);
UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
@@ -6894,7 +6967,11 @@ void DBDriverSqlite3::stepWordsChanged(sqlite3_stmt * ppStmt, int nodeId, int ol
std::string DBDriverSqlite3::queryStepKeypoint() const
{
if(uStrNumCmp(_version, "0.13.0") >= 0)
if(uStrNumCmp(_version, "0.24.0") >= 0)
{
return "INSERT INTO Feature(node_id, word_id, index) VALUES(?,?,?);";
}
else if(uStrNumCmp(_version, "0.13.0") >= 0)
{
return "INSERT INTO Feature(node_id, word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?);";
}
@@ -6908,6 +6985,32 @@ std::string DBDriverSqlite3::queryStepKeypoint() const
}
return "INSERT INTO Map_Node_Word(node_id, word_id, pos_x, pos_y, size, dir, response, depth_x, depth_y, depth_z) VALUES(?,?,?,?,?,?,?,?,?,?);";
}
void DBDriverSqlite3::stepKeypoint(sqlite3_stmt * ppStmt,
int nodeId,
int wordId,
int kptIndex) const
{
// Used with version >= 0.24
UASSERT(uStrNumCmp(_version, "0.24.0") >= 0);
if(!ppStmt)
{
UFATAL("");
}
int rc = SQLITE_OK;
int index = 1;
rc = sqlite3_bind_int(ppStmt, index++, nodeId);
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
rc = sqlite3_bind_int(ppStmt, index++, wordId);
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
rc = sqlite3_bind_double(ppStmt, index++, kptIndex);
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
rc=sqlite3_step(ppStmt);
UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
rc = sqlite3_reset(ppStmt);
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
}
void DBDriverSqlite3::stepKeypoint(sqlite3_stmt * ppStmt,
int nodeId,
int wordId,
@@ -6915,6 +7018,8 @@ void DBDriverSqlite3::stepKeypoint(sqlite3_stmt * ppStmt,
const cv::Point3f & pt,
const cv::Mat & descriptor) const
{
// Used with version < 0.24
UASSERT(uStrNumCmp(_version, "0.24.0") < 0);
if(!ppStmt)
{
UFATAL("");

View File

@@ -45,6 +45,8 @@ CREATE TABLE Data (
view_point_x FLOAT,
view_point_y FLOAT,
view_point_z FLOAT,
features BLOB, -- compressed serialized data (pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor)
user_data BLOB, -- compressed data (User data)
time_enter DATE,
@@ -74,17 +76,7 @@ CREATE TABLE Word (
CREATE TABLE Feature (
node_id INTEGER NOT NULL,
word_id INTEGER NOT NULL,
pos_x FLOAT NOT NULL,
pos_y FLOAT NOT NULL,
size INTEGER NOT NULL,
dir FLOAT NOT NULL,
response FLOAT NOT NULL,
octave INTEGER NOT NULL,
depth_x FLOAT,
depth_y FLOAT,
depth_z FLOAT,
descriptor_size INTEGER,
descriptor BLOB,
index INTEGER NOT NULL, -- index of the feature in "features" field of Data
FOREIGN KEY (node_id) REFERENCES Node(id)
);

View File

@@ -0,0 +1,185 @@
-- *******************************************************************
-- DatabaseSchema: Script for creating the database
-- Usage:
-- $ sqlite3 LTM.db < DatabaseSchema.sql
--
-- *******************************************************************
-- *******************************************************************
-- CLEAN
-- *******************************************************************
/*DROP TABLE Node;*/
-- *******************************************************************
-- CREATE
-- *******************************************************************
CREATE TABLE Node (
id INTEGER NOT NULL,
map_id INTEGER NOT NULL,
weight INTEGER,
stamp FLOAT,
pose BLOB, -- 3x4 float
ground_truth_pose BLOB, -- 3x4 float
velocity BLOB, -- 6 float (vx,vy,vz,vroll,vpitch,vyaw) m/s and rad/s
label TEXT,
gps BLOB, -- 1x6 double: stamp, longitude (DD), latitude (DD), altitude (m), accuracy (m), bearing (North 0->360 deg clockwise)
env_sensors BLOB, -- Variable 3xdouble: (sensorId1, value, stamp, sensorId2, value, stamp, ...)
time_enter DATE,
PRIMARY KEY (id)
);
CREATE TABLE Data (
id INTEGER NOT NULL,
image BLOB, -- compressed image (Grayscale or RGB)
depth BLOB, -- compressed image (Depth or Right image)
depth_confidence BLOB, -- compressed data (low=0 high=100)
calibration BLOB, -- fx, fy, cx, cy, [baseline,] width, height, local_transform
scan BLOB, -- compressed data (Laser scan)
scan_info BLOB, -- scan_max_pts, scan_max_range, scan_format, local_transform
ground_cells BLOB, -- compressed data (occupancy grid)
obstacle_cells BLOB, -- compressed data (occupancy grid)
empty_cells BLOB, -- compressed data (occupancy grid)
cell_size FLOAT,
view_point_x FLOAT,
view_point_y FLOAT,
view_point_z FLOAT,
user_data BLOB, -- compressed data (User data)
time_enter DATE,
PRIMARY KEY (id)
);
CREATE TABLE Link (
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
type INTEGER NOT NULL, -- kNeighbor=0, kGlobalClosure=1, kLocalSpaceClosure=2, kLocalTimeClosure=3, kUserClosure=4, kVirtualClosure=5, kNeighborMerged=6, kPosePrior=7, kLandmark=8
information_matrix BLOB NOT NULL, -- 6x6 double (inverse covariance)
transform BLOB, -- 3x4 float
user_data BLOB, -- compressed data (User data)
FOREIGN KEY (from_id) REFERENCES Node(id),
FOREIGN KEY (to_id) REFERENCES Node(id)
);
--
CREATE TABLE Word (
id INTEGER NOT NULL,
descriptor_size INTEGER NOT NULL,
descriptor BLOB NOT NULL,
time_enter DATE,
PRIMARY KEY (id)
);
CREATE TABLE Feature (
node_id INTEGER NOT NULL,
word_id INTEGER NOT NULL,
pos_x FLOAT NOT NULL,
pos_y FLOAT NOT NULL,
size INTEGER NOT NULL,
dir FLOAT NOT NULL,
response FLOAT NOT NULL,
octave INTEGER NOT NULL,
depth_x FLOAT,
depth_y FLOAT,
depth_z FLOAT,
descriptor_size INTEGER,
descriptor BLOB,
FOREIGN KEY (node_id) REFERENCES Node(id)
);
CREATE TABLE GlobalDescriptor (
node_id INTEGER NOT NULL,
type INTEGER NOT NULL,
info BLOB,
data BLOB NOT NULL,
FOREIGN KEY (node_id) REFERENCES Node(id)
);
--
CREATE TABLE Info (
STM_size INTEGER,
last_sign_added INTEGER,
process_mem_used INTEGER,
database_mem_used INTEGER,
dictionary_size INTEGER,
parameters TEXT,
time_enter DATE
);
CREATE TABLE Statistics (
id INTEGER NOT NULL,
stamp FLOAT,
data BLOB, -- compressed string
wm_state BLOB, -- compressed data
FOREIGN KEY (id) REFERENCES Node(id)
);
CREATE TABLE Admin (
version TEXT,
preview_image BLOB, -- compressed image
opt_cloud BLOB, -- compressed data
opt_ids BLOB, -- Node ids used to generate the optimized cloud/mesh
opt_poses BLOB, -- compressed N*3x4 float
opt_last_localization BLOB, -- 3x4 float
opt_polygons_size INTEGER, -- e.g., 3
opt_polygons BLOB, -- compressed data [length_v0, i0,i1,i3, length_v1, i0,i1,i3]
opt_tex_coords BLOB, -- compressed data [length_v0, u0,v0,u1,v1,u2,v2, length_v1, u0,v0,u1,v1,u2,v2]
opt_tex_materials BLOB, -- compressed image
opt_map BLOB, -- compressed CV_8SC1 occupancy grid
opt_map_x_min FLOAT,
opt_map_y_min FLOAT,
opt_map_resolution FLOAT,
dictionary_index BLOB, -- serialized dictionary index
time_enter DATE
);
-- *******************************************************************
-- TRIGGERS
-- *******************************************************************
CREATE TRIGGER insert_Feature BEFORE INSERT ON Feature
WHEN NOT EXISTS (SELECT Node.id FROM Node WHERE Node.id = NEW.node_id)
BEGIN
SELECT RAISE(ABORT, 'Foreign key constraint failed in Feature table');
END;
-- Creating a trigger for time_enter
CREATE TRIGGER insert_Node_timeEnter AFTER INSERT ON Node
BEGIN
UPDATE Node SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid;
END;
CREATE TRIGGER insert_Data_timeEnter AFTER INSERT ON Data
BEGIN
UPDATE Node SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid;
END;
CREATE TRIGGER insert_Word_timeEnter AFTER INSERT ON Word
BEGIN
UPDATE Word SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid;
END;
CREATE TRIGGER insert_Info_timeEnter AFTER INSERT ON Info
BEGIN
UPDATE Info SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid;
END;
-- *******************************************************************
-- INDEXES
-- *******************************************************************
CREATE UNIQUE INDEX IDX_Node_id on Node (id);
CREATE INDEX IDX_Feature_node_id on Feature (node_id);
CREATE INDEX IDX_GlobalDescriptor_node_id on GlobalDescriptor (node_id);
CREATE INDEX IDX_Link_from_id on Link (from_id);
CREATE UNIQUE INDEX IDX_node_label on Node (label);
CREATE UNIQUE INDEX IDX_Statistics_id on Statistics (id);
-- *******************************************************************
-- VERSION
-- *******************************************************************
INSERT INTO Admin(version) VALUES('0.23.4');