From: Sandro Santilli Date: Wed, 25 Feb 2015 08:46:08 +0000 (+0000) Subject: Add N-dimensional distance operator with KNN support X-Git-Tag: 2.2.0rc1~633 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=19bd4ed0162be01f14bc298e4bffcf1d46dcd558;p=postgis Add N-dimensional distance operator with KNN support Includes docs and tests git-svn-id: http://svn.osgeo.org/postgis/trunk@13287 b70326c6-7e19-0410-871a-916f4a2858ee --- diff --git a/NEWS b/NEWS index cd48d1e82..b9b4ae18d 100644 --- a/NEWS +++ b/NEWS @@ -26,6 +26,8 @@ PostGIS 2.2.0 * New Features * + - #3040, KNN GiST index based centroid (<<->>) and box (<<#>>) + n-D distance operators (Sandro Santilli / Boundless) - Interruptibility API for liblwgeom (Sandro Santilli / CartoDB) - #2939, ST_ClipByBox2D (Sandro Santilli / CartoDB) - #2247, ST_Retile and ST_CreateOverview: in-db raster overviews creation diff --git a/doc/reference_operator.xml b/doc/reference_operator.xml index 9c2e63bea..36bc7f7c6 100644 --- a/doc/reference_operator.xml +++ b/doc/reference_operator.xml @@ -1197,7 +1197,7 @@ Finally the hybrid: <#> -Returns the 2D distance between bounding boxes of 2 geometries. +Returns the 2D distance between A and B bounding boxes. @@ -1272,4 +1272,126 @@ SELECT b.tlid, b.mtfcc, + + + <<->> + + +Returns the n-D distance between the centroids of A and B bounding +boxes. + + + + + + + double precision <<->> + + + geometry + + A + + + + geometry + + B + + + + + + + Description + + +The <<->> operator returns the n-D (euclidean) +distance between the centroids of the bounding boxes of two geometries. +Useful for doing nearest neighbor +approximate distance ordering. + + + +This operand will make use of n-D GiST indexes that may be available on +the geometries. It is different from other operators that use spatial +indexes in that the spatial index is only used when the operator is in +the ORDER BY clause. + + +Index only kicks in if one of the geometries is a constant (not in a +subquery/cte). e.g. 'SRID=3005;POINT(1011102 450541)'::geometry instead +of a.geom + + + Availability: 2.2.0 -- KNN only available for PostgreSQL 9.1+ + + + + + + See Also + +, + + + + + + + + <<#>> + + +Returns the n-D distance between A and B bounding boxes. + + + + + + + double precision <<#>> + + + geometry + + A + + + + geometry + + B + + + + + + + Description + + The <<#>> operator returns distance between two floating point bounding boxes, possibly reading them from a spatial index (PostgreSQL 9.1+ required). Useful for doing nearest neighbor approximate distance ordering. + + This operand will make use of any indexes that may be available on the + geometries. It is different from other operators that use spatial indexes in that the spatial index is only used when the operator + is in the ORDER BY clause. + +Index only kicks in if one of the geometries is a constant e.g. ORDER BY +(ST_GeomFromText('POINT(1 2)') <<#>> geom) instead of g1.geom +<<#>>. + + + Availability: 2.2.0 -- KNN only available for PostgreSQL 9.1+ + + + + + See Also + +, + + + + + diff --git a/postgis/gserialized_gist_nd.c b/postgis/gserialized_gist_nd.c index e264cf374..066b780fe 100644 --- a/postgis/gserialized_gist_nd.c +++ b/postgis/gserialized_gist_nd.c @@ -36,6 +36,9 @@ #include "gserialized_gist.h" /* For utility functions. */ #include "geography.h" +#include + + /* Fall back to older finite() if necessary */ #ifndef HAVE_ISFINITE # ifdef HAVE_GNU_ISFINITE @@ -75,6 +78,7 @@ Datum gserialized_gist_penalty(PG_FUNCTION_ARGS); Datum gserialized_gist_picksplit(PG_FUNCTION_ARGS); Datum gserialized_gist_union(PG_FUNCTION_ARGS); Datum gserialized_gist_same(PG_FUNCTION_ARGS); +Datum gserialized_gist_distance(PG_FUNCTION_ARGS); /* ** ND Operator prototypes @@ -82,6 +86,8 @@ Datum gserialized_gist_same(PG_FUNCTION_ARGS); Datum gserialized_overlaps(PG_FUNCTION_ARGS); Datum gserialized_contains(PG_FUNCTION_ARGS); Datum gserialized_within(PG_FUNCTION_ARGS); +Datum gserialized_distance_box_nd(PG_FUNCTION_ARGS); +Datum gserialized_distance_centroid_nd(PG_FUNCTION_ARGS); /* ** GIDX true/false test function type @@ -456,6 +462,98 @@ gserialized_datum_predicate(Datum gs1, Datum gs2, gidx_predicate predicate) return LW_FALSE; } +/** +* Calculate the centroid->centroid distance between the boxes. +*/ +static double gidx_distance_leaf_centroid(const GIDX *a, const GIDX *b) +{ + int ndims, i; + double sum = 0; + + /* Base computation on least available dimensions */ + ndims = Min(GIDX_NDIMS(b), GIDX_NDIMS(a)); + for ( i = 0; i < ndims; ++i ) + { + double ca, cb, d; + double amin = GIDX_GET_MIN(a,i); + double amax = GIDX_GET_MAX(a,i); + double bmin = GIDX_GET_MIN(b,i); + double bmax = GIDX_GET_MAX(b,i); + ca = amin + ( ( amax - amin ) / 2.0 ); + cb = bmin + ( ( bmax - bmin ) / 2.0 ); + d = ca - cb; + if ( ! isfinite(d) ) + { + /* Can happen if a dimension was padded with FLT_MAX, + * effectively meaning "infinite range". In that case + * we take that dimension as adding 0 to the total + * distance. + */ + continue; + } + sum += d * d; +/* + POSTGIS_DEBUGF(3, " centroid of A for dimension %d is %g", i, ca); + POSTGIS_DEBUGF(3, " centroid of B for dimension %d is %g", i, cb); + POSTGIS_DEBUGF(3, " distance on dimension %d is %g, squared as %g, grows sum to %g", i, d, d*d, sum); +*/ + } + return sqrt(sum); +} + +/** +* Calculate the box->box distance. +*/ +static double gidx_distance(const GIDX *a, const GIDX *b) +{ + int ndims, i; + double sum = 0; + + /* Base computation on least available dimensions */ + ndims = Min(GIDX_NDIMS(b), GIDX_NDIMS(a)); + for ( i = 0; i < ndims; ++i ) + { + double d; + double amin = GIDX_GET_MIN(a,i); + double amax = GIDX_GET_MAX(a,i); + double bmin = GIDX_GET_MIN(b,i); + double bmax = GIDX_GET_MAX(b,i); + POSTGIS_DEBUGF(3, "A %g - %g", amin, amax); + POSTGIS_DEBUGF(3, "B %g - %g", bmin, bmax); + + if ( ( amin <= bmax && amax >= bmin ) ) + { + /* overlaps */ + d = 0; + } + else if ( bmax < amin ) + { + /* is "left" */ + d = amin - bmax; + } + else + { + /* is "right" */ + assert( bmin > amax ); + d = bmin - amax; + } + if ( ! isfinite(d) ) + { + /* Can happen if coordinates are corrupted/NaN */ + continue; + } + sum += d * d; + POSTGIS_DEBUGF(3, "dist %g, squared %g, grows sum to %g", d, d*d, sum); + } + return sqrt(sum); +} + +static double gidx_distance_node_centroid(const GIDX *node, const GIDX *query) +{ + /* TODO: implement ! */ + return 0; +} + /** * Return a #GSERIALIZED with an expanded bounding box. */ @@ -482,6 +580,55 @@ gserialized_expand(GSERIALIZED *g, double distance) * GiST N-D Index Operator Functions */ +PG_FUNCTION_INFO_V1(gserialized_distance_box_nd); +Datum gserialized_distance_box_nd(PG_FUNCTION_ARGS) +{ + char bmem1[GIDX_MAX_SIZE]; + GIDX *b1 = (GIDX*)bmem1; + char bmem2[GIDX_MAX_SIZE]; + GIDX *b2 = (GIDX*)bmem2; + Datum gs1 = PG_GETARG_DATUM(0); + Datum gs2 = PG_GETARG_DATUM(1); + double distance; + + POSTGIS_DEBUG(3, "entered function"); + + /* Must be able to build box for each argument (ie, not empty geometry). */ + if ( (gserialized_datum_get_gidx_p(gs1, b1) == LW_SUCCESS) && + (gserialized_datum_get_gidx_p(gs2, b2) == LW_SUCCESS) ) + { + distance = gidx_distance(b1, b2); + POSTGIS_DEBUGF(3, "got boxes %s and %s", gidx_to_string(b1), gidx_to_string(b2)); + PG_RETURN_FLOAT8(distance); + } + PG_RETURN_FLOAT8(FLT_MAX); +} + + +PG_FUNCTION_INFO_V1(gserialized_distance_centroid_nd); +Datum gserialized_distance_centroid_nd(PG_FUNCTION_ARGS) +{ + char b1mem[GIDX_MAX_SIZE]; + GIDX *b1 = (GIDX*)b1mem; + char b2mem[GIDX_MAX_SIZE]; + GIDX *b2 = (GIDX*)b2mem; + Datum gs1 = PG_GETARG_DATUM(0); + Datum gs2 = PG_GETARG_DATUM(1); + double distance; + + POSTGIS_DEBUG(3, "entered function"); + + /* Must be able to build box for each argument (ie, not empty geometry). */ + if ( (gserialized_datum_get_gidx_p(gs1, b1) == LW_SUCCESS) && + (gserialized_datum_get_gidx_p(gs2, b2) == LW_SUCCESS) ) + { + distance = gidx_distance_leaf_centroid(b1, b2); + POSTGIS_DEBUGF(3, "got boxes %s and %s", gidx_to_string(b1), gidx_to_string(b2)); + PG_RETURN_FLOAT8(distance); + } + PG_RETURN_FLOAT8(FLT_MAX); +} + /* ** '~' and operator function. Based on two serialized return true if ** the first is contained by the second. @@ -842,8 +989,6 @@ Datum gserialized_gist_union(PG_FUNCTION_ARGS) } - - /* ** GiST support function. Test equality of keys. */ @@ -861,6 +1006,72 @@ Datum gserialized_gist_same(PG_FUNCTION_ARGS) PG_RETURN_POINTER(result); } +/* +** GiST support function. +** Take in a query and an entry and return the "distance" between them. +** +** Given an index entry p and a query value q, this function determines the +** index entry's "distance" from the query value. This function must be +** supplied if the operator class contains any ordering operators. A query +** using the ordering operator will be implemented by returning index entries +** with the smallest "distance" values first, so the results must be consistent +** with the operator's semantics. For a leaf index entry the result just +** represents the distance to the index entry; for an internal tree node, the +** result must be the smallest distance that any child entry could have. +** +** Strategy 13 = centroid-based distance tests +** Strategy 14 = box-based distance tests (not implemented) +*/ +PG_FUNCTION_INFO_V1(gserialized_gist_distance); +Datum gserialized_gist_distance(PG_FUNCTION_ARGS) +{ + GISTENTRY *entry = (GISTENTRY*) PG_GETARG_POINTER(0); + StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); + char query_box_mem[GIDX_MAX_SIZE]; + GIDX *query_box = (GIDX*)query_box_mem; + GIDX *entry_box; + double distance; + + POSTGIS_DEBUG(4, "[GIST] 'distance' function called"); + + /* We are using '13' as the gist distance-betweeen-centroids strategy number + * and '14' as the gist distance-between-boxes strategy number */ + if ( strategy != 13 && strategy != 14 ) { + elog(ERROR, "unrecognized strategy number: %d", strategy); + PG_RETURN_FLOAT8(FLT_MAX); + } + + /* Null box should never make this far. */ + if ( gserialized_datum_get_gidx_p(PG_GETARG_DATUM(1), query_box) == LW_FAILURE ) + { + POSTGIS_DEBUG(4, "[GIST] null query_gbox_index!"); + PG_RETURN_FLOAT8(FLT_MAX); + } + + /* Get the entry box */ + entry_box = (GIDX*)DatumGetPointer(entry->key); + + /* Box-style distance test */ + if ( strategy == 14 ) + { + distance = gidx_distance(entry_box, query_box); + PG_RETURN_FLOAT8(distance); + } + + /* Treat leaf node tests different from internal nodes */ + if (GIST_LEAF(entry)) + { + /* Calculate distance to leaves */ + distance = (double)gidx_distance_leaf_centroid(entry_box, query_box); + } + else + { + /* Calculate distance for internal nodes */ + distance = (double)gidx_distance_node_centroid(entry_box, query_box); + } + + PG_RETURN_FLOAT8(distance); +} /* diff --git a/postgis/postgis.sql.in b/postgis/postgis.sql.in index dbac31b2c..654b7c010 100644 --- a/postgis/postgis.sql.in +++ b/postgis/postgis.sql.in @@ -809,6 +809,39 @@ CREATE OPERATOR &&& ( JOIN = gserialized_gist_joinsel_nd ); +-- Availability: 2.2.0 +CREATE OR REPLACE FUNCTION geometry_distance_centroid_nd(geometry,geometry) + RETURNS float8 + AS 'MODULE_PATHNAME', 'gserialized_distance_centroid_nd' + LANGUAGE 'c' IMMUTABLE STRICT; + +-- Availability: 2.2.0 +CREATE OPERATOR <<->> ( + LEFTARG = geometry, RIGHTARG = geometry, + PROCEDURE = geometry_distance_centroid_nd, + COMMUTATOR = '<<->>' +); + +-- Availability: 2.2.0 +CREATE OR REPLACE FUNCTION geometry_distance_box_nd(geom1 geometry, geom2 geometry) + RETURNS float8 + AS 'MODULE_PATHNAME' ,'gserialized_distance_box_nd' + LANGUAGE 'c' IMMUTABLE STRICT; + +-- Availability: 2.2.0 +CREATE OPERATOR <<#>> ( + LEFTARG = geometry, RIGHTARG = geometry, + PROCEDURE = geometry_distance_box_nd, + COMMUTATOR = '<<#>>' +); + +-- Availability: 2.2.0 +CREATE OR REPLACE FUNCTION geometry_gist_distance_nd(internal,geometry,int4) + RETURNS float8 + AS 'MODULE_PATHNAME', 'gserialized_gist_distance' + LANGUAGE 'c'; + + -- Availability: 2.0.0 CREATE OPERATOR CLASS gist_geometry_ops_nd FOR TYPE geometry USING GIST AS @@ -817,6 +850,14 @@ CREATE OPERATOR CLASS gist_geometry_ops_nd -- OPERATOR 6 ~= , -- OPERATOR 7 ~ , -- OPERATOR 8 @ , +#if POSTGIS_PGSQL_VERSION >= 91 + -- Availability: 2.2.0 + OPERATOR 13 <<->> FOR ORDER BY pg_catalog.float_ops, + -- Availability: 2.2.0 + OPERATOR 14 <<#>> FOR ORDER BY pg_catalog.float_ops, + -- Availability: 2.2.0 + FUNCTION 8 geometry_gist_distance_nd (internal, geometry, int4), +#endif FUNCTION 1 geometry_gist_consistent_nd (internal, geometry, int4), FUNCTION 2 geometry_gist_union_nd (bytea, internal), FUNCTION 3 geometry_gist_compress_nd (internal), diff --git a/regress/knn.sql b/regress/knn.sql index df0929b0c..9ef9fab85 100644 --- a/regress/knn.sql +++ b/regress/knn.sql @@ -21,10 +21,11 @@ END; $$; \i regress_lots_of_points.sql -CREATE INDEX on test using gist (the_geom); -- Index-supported KNN query +CREATE INDEX test_gist_2d on test using gist (the_geom); + SELECT '<-> idx', qnodes('select * from test order by the_geom <-> ST_MakePoint(0,0) LIMIT 1'); SELECT '<-> res1',num, (the_geom <-> 'LINESTRING(0 0,5 5)'::geometry)::numeric(10,2), @@ -38,6 +39,67 @@ SELECT '<#> res1',num, ST_astext(the_geom) from test order by the_geom <#> 'LINESTRING(1000 0,1005 5)'::geometry LIMIT 1; +-- Index-supported nd-KNN query + +DROP INDEX test_gist_2d; + +UPDATE test set the_geom = ST_MakePoint( + ST_X(the_geom), ST_Y(the_geom), + num, -num); + +SELECT '<<->> seq', qnodes('select * from test order by the_geom <<->> ST_MakePoint(0,0)'); +SELECT '<<#>> seq', qnodes('select * from test order by the_geom <<#>> ST_MakePoint(0,0)'); + +CREATE INDEX test_gist_nd on test using gist (the_geom gist_geometry_ops_nd); + +ANALYZE test; + +-- EXT X Y Z M +-- min 0.0439142361 | 0.0197799355| 1| -50000 +-- max 999.955261 | 999.993652 | 50000| -1 +--SELECT min(st_x(the_geom)) as minx, min(st_y(the_geom)) as miny, +-- min(st_z(the_geom)) as minz, min(st_m(the_geom)) as minm, +-- max(st_x(the_geom)) as maxx, max(st_y(the_geom)) as maxy, +-- max(st_z(the_geom)) as maxz, max(st_m(the_geom)) as maxm +--FROM test; + + +SELECT '<<->> idx', qnodes('select * from test order by the_geom <<->> ST_MakePoint(0,0) LIMIT 1'); +SELECT '<<->> res1',num, + (the_geom <<->> 'LINESTRING(0 0,5 5)'::geometry)::numeric(10,2), + ST_astext(the_geom) from test + order by the_geom <<->> 'LINESTRING(0 0,5 5)'::geometry LIMIT 1; +SELECT '<<->> res2',num, + (the_geom <<->> 'POINT(95 23 25024 -25025)'::geometry)::numeric(10,2), + ST_astext(the_geom) from test + order by the_geom <<->> 'POINT(95 23 25024 -25025)'::geometry LIMIT 1; +SELECT '<<->> res3',num, + (the_geom <<->> 'POINT(631 729 25023 -25022)'::geometry)::numeric(10,2), + ST_astext(the_geom) from test + order by the_geom <<->> 'POINT(631 729 25023 -25022)'::geometry LIMIT 1; + +-- EXT X Y Z M +-- min 0.0439142361 | 0.0197799355| 1| -50000 +-- max 999.955261 | 999.993652 | 50000| -1 +SELECT '<<#>> idx', qnodes('select * from test order by the_geom <<#>> ST_MakePoint(0,0) LIMIT 1'); +SELECT '<<#>> res1',num, + (the_geom <<#>> 'LINESTRING(1000 0,1005 5)'::geometry)::numeric(10,2), + ST_astext(the_geom) from test + order by the_geom <<#>> 'LINESTRING(1000 0,1005 5)'::geometry LIMIT 1; +-- <<#>> res2|1|2.00|POINT ZM (529.522339 509.260284 1 -1) +SELECT '<<#>> res2',num, + (the_geom <<#>> 'LINESTRING ZM (0 0 -10 -10,1000 1000 -1 -1)'::geometry)::numeric(10,2), + ST_astext(the_geom) from test + order by the_geom <<#>> 'LINESTRING ZM (0 0 -10 -10,1000 1000 -1 -1)'::geometry LIMIT 1; +-- <<#>> res3|50000|1.00|POINT ZM (912.12323 831.139587 50000 -50000) +SELECT '<<#>> res3',num, + (the_geom <<#>> 'LINESTRING ZM (0 0 1 -60000,1000 1000 50000 -50001)'::geometry)::numeric(10,2), + ST_astext(the_geom) from test + order by the_geom <<#>> 'LINESTRING ZM (0 0 1 -60000,1000 1000 50000 -50001)'::geometry LIMIT 1; + + +-- Cleanup + DROP FUNCTION qnodes(text); DROP TABLE test; diff --git a/regress/knn_expected b/regress/knn_expected index 5a63f278f..209fcf014 100644 --- a/regress/knn_expected +++ b/regress/knn_expected @@ -2,3 +2,13 @@ <-> res1|48589|0.17|POINT(2.33793712 2.44566727) <#> idx|Index Scan <#> res1|2057|0.83|POINT(999.173279 3.92185807) +<<->> seq|Seq Scan +<<#>> seq|Seq Scan +<<->> idx|Index Scan +<<->> res1|48589|0.17|POINT ZM (2.33793712 2.44566727 48589 -48589) +<<->> res2|25025|1.20|POINT ZM (95.6546249 23.0995369 25025 -25025) +<<->> res3|25023|1.27|POINT ZM (631.060242 729.787354 25023 -25023) +<<#>> idx|Index Scan +<<#>> res1|2057|0.83|POINT ZM (999.173279 3.92185807 2057 -2057) +<<#>> res2|1|2.00|POINT ZM (529.522339 509.260284 1 -1) +<<#>> res3|50000|1.00|POINT ZM (912.12323 831.139587 50000 -50000) diff --git a/regress/operators.sql b/regress/operators.sql index 121ffc776..5ec6b2e9c 100644 --- a/regress/operators.sql +++ b/regress/operators.sql @@ -141,3 +141,41 @@ WITH v(i,g) AS ( VALUES ) SELECT 'ndovm2', array_agg(i) FROM v WHERE g &&& 'POINTZ(0 0 1)'::geometry ORDER BY 1; + +-- nd box centroid distance <<->> + +select 'ndcd1', 'LINESTRING(0 0,0 10,10 10)'::geometry <<->> + 'LINESTRING(6 2,6 8)'::geometry; -- 1 +select 'ndcd2', 'LINESTRING(0 0,0 10,10 10)'::geometry <<->> + 'LINESTRING(11 0,19 10)'::geometry; -- 10 +select 'ndcd3', 'POINTM(0 0 0)'::geometry <<->> + 'POINTM(0 0 5)'::geometry; -- 5 +select 'ndcd4', 'POINTZ(0 0 15)'::geometry <<->> + 'POINTZ(0 0 10)'::geometry; -- 5 +select 'ndcd5', 'POINTZM(1 2 3 4)'::geometry <<->> + 'POINTZM(2 3 4 5)'::geometry; -- 2 +select 'ndcd6', 'POINTZM(9 9 3 4)'::geometry <<->> + 'POINT(9 8)'::geometry; -- 1, higher dimensions overlapping + +-- nd box distance <<#>> + +select 'ndbd1', 'LINESTRING(0 0,0 10,10 10)'::geometry <<#>> + 'LINESTRING(6 2,6 8)'::geometry; -- 0, overlap +select 'ndbd2', 'LINESTRING(0 0,0 10,10 10)'::geometry <<#>> + 'LINESTRING(11 0,19 10)'::geometry; -- 1 on the right +select 'ndbd3', 'LINESTRING(0 0,10 10)'::geometry <<#>> + 'LINESTRING(-11 0,-2 10)'::geometry; -- 2 on the left +select 'ndbd4', 'LINESTRING(0 0,10 10)'::geometry <<#>> + 'LINESTRING(0 13,5 14)'::geometry; -- 3 above +select 'ndbd5', 'LINESTRING(0 0,10 10)'::geometry <<#>> + 'LINESTRING(0 -20,5 -4)'::geometry; -- 4 below +select 'ndbd6', 'LINESTRINGM(0 0 0,1 1 1)'::geometry <<#>> + 'LINESTRING(0 0,1 1)'::geometry; -- 0 overlap, mixed +select 'ndbd7', 'LINESTRINGM(0 0 0,1 1 1)'::geometry <<#>> + 'LINESTRINGM(1 1 2,1 1 3)'::geometry; -- 1 +select 'ndbd8', 'LINESTRINGZ(0 0 0,1 1 1)'::geometry <<#>> + 'LINESTRINGZ(1 1 3,1 1 5)'::geometry; -- 2 +select 'ndbd9', 'LINESTRINGZ(0 0 0,1 1 1)'::geometry <<#>> + 'LINESTRINGM(1 1 3,1 1 5)'::geometry; -- 0, overlap, mixed +select 'ndbd10', 'LINESTRINGZM(0 0 0 0,1 2 3 4)'::geometry <<#>> + 'LINESTRINGZM(3 4 5 6,4 5 6 7)'::geometry; -- 4 diff --git a/regress/operators_expected b/regress/operators_expected index f8b1dc8bf..c3f399861 100644 --- a/regress/operators_expected +++ b/regress/operators_expected @@ -57,3 +57,19 @@ ndov6|t ndov7|t ndovm1|{1,2,3,4,5,8} ndovm2|{1,2,4,6,7} +ndcd1|1 +ndcd2|10 +ndcd3|5 +ndcd4|5 +ndcd5|2 +ndcd6|1 +ndbd1|0 +ndbd2|1 +ndbd3|2 +ndbd4|3 +ndbd5|4 +ndbd6|0 +ndbd7|1 +ndbd8|2 +ndbd9|0 +ndbd10|4